Service configuration (workspace/services/<name>/service.yml)
Service declarations for the DWE project.
Contents
Section titled “Contents”Purpose
Section titled “Purpose”Service definitions live under workspace/services/, one folder per service. Each service is declared in workspace/services/<name>/service.yml, where <name> becomes the service’s key in the resolved DweConfig.Services map. Each entry declares its container name, ports, hosts, compose overlay, and optional structural fields. The type: discriminator selects which fields are legal for the entry.
On load, each workspace/services/*/ subdirectory is enumerated and its service.yml is parsed in strict mode: the file must declare a type, every field is checked against the type’s allowlist, extends: is rejected on non-app entries, and the shapes of ports / hosts are validated. Cross-service extends: chains are then resolved in topological order and parent fields are merged into each child. A missing workspace/services/ directory yields an empty service set (not an error). Per-developer toggles (enabled:, ports:, hosts:) live in the 3-layer overlay; structural fields belong exclusively in workspace/services/<name>/service.yml.
Service types
Section titled “Service types”Every entry under services: requires a type: key. Three values are supported:
type: | Semantics | Deploy lifecycle | depends_on: target | App-only fields |
|---|---|---|---|---|
app | Service with source code under dir:; renders IDE/AI/git templates; runs through dwe deploy. | yes (a workspace/services/<name>/deploy.yml may exist) | yes | yes (dir, dir_internal, work_dir_internal, configs, dirs, extends, cli, render, generated) |
tool | Ephemeral utility container (adminer, mailpit, redis-insight). Cannot be a dependency target of any service. | no | no | no |
infra | Backing service (db, cache, queue, search). Can be a dependency target of app / other infra. | no | yes | no |
Locked rules:
extends:is app-only. Atool/infraentry withextends:is rejected at load.depends_on:may not reference atype: toolentry. This is enforced at load (ErrDependsOnTool), not only at validate time.workspace/services/<name>/deploy.ymlis supported for any service type (app, tool, infra). Full deploy (dwe deploy run) enumerates every enabled service that has adeploy.yml;dwe deploy run --service <name>works for any service type with a deploy file regardless of enabled state.ports:is alwaysmap[string]intandhosts:is alwaysmap[string]string. There is noport:/host:scalar shorthand. A single-port entry writesports: { http: 8025 }.- Type semantics partition
docker composefile emission totool → infra → apporder.
type: infra services may be optional (required: false) — they take a compose: overlay and are toggleable via dwe services enable|disable <name> like apps and tools. Required infra (required: true, typical for backing services like databases, caches, and queues) is always-on and not toggleable. Optional infra fits semantically request-path or data-path components that are not strictly required for every developer (e.g. a Varnish cache in front of nginx, or a MinIO S3-storage backend used only when no external S3 is configured).
Why the type matrix
Section titled “Why the type matrix”The per-type allowlists reflect the distinct roles services play in the dev environment:
app: owns source code and a running container. Supports mounts, templates, and deploy orchestration (dir,extends,configs,depends_on).tool: standalone utility container (database UI, observability frontend, etc.). No source ownership; cannot depend on services or be adepends_ontarget for other services.infra: supporting container (database, cache, broker, reverse proxy). Can be adepends_ontarget but not own source; lighter footprint than app.
This separation ensures that build and deploy logic is explicit (defined only in type: app), and that infrastructure services remain independently testable without dragging in application code.
Per-type field allowlist
Section titled “Per-type field allowlist”| Field | app | tool | infra |
|---|---|---|---|
type | ✓ | ✓ | ✓ |
container | ✓ | ✓ | ✓ |
required | ✓ | ✓ | ✓ |
compose | ✓ | ✓ | ✓ |
ports | ✓ | ✓ | ✓ |
hosts | ✓ | ✓ | ✓ |
icon | ✓ | ✓ | ✓ |
info | ✓ | ✓ | ✓ |
depends_on | ✓ | — | ✓ |
status | ✓ | ✓ | ✓ |
dir | ✓ | — | — |
dir_internal | ✓ | — | — |
work_dir_internal | ✓ | — | — |
configs | ✓ | — | — |
dirs | ✓ | — | — |
extends | ✓ | — | — |
cli | ✓ | — | — |
render | ✓ | — | — |
generated | ✓ | — | — |
on_enable | ✓ | ✓ | ✓ |
on_disable | ✓ | ✓ | ✓ |
notes | ✓ | ✓ | ✓ |
bridge | ✓ | ✓ | ✓ |
A disallowed field is a hard load error (ErrServiceFieldNotAllowed). Validation aggregates per-file violations via errors.Join so a single parse pass surfaces every issue at once.
Load behavior
Section titled “Load behavior”- Each
workspace/services/<name>/service.ymlis strict-decoded — unknown and per-type-disallowed fields are hard errors. Errors from all folders are reported together, so every broken folder surfaces at once, not just the first. - Service inheritance via
extends:is resolved in topological order (parents before children) so multi-level chains (C → B → A) merge correctly regardless of map iteration order. Cycles and unknown parents are reported as load errors.extends:is app-only. - For each child, only zero-value fields are inherited from the parent; child fields take precedence on conflicts. Inherited slices and maps are copied defensively, so mutating a child never corrupts the parent.
- The
dirsfield is deduplicated across parent and child (parent first, child appended).cli.envis recursively merged: parent provides defaults, child wins on key conflicts. - After loading,
enabledis resolved from the 3-layer merge (services.<name>.enabled); required services forceenabled: true. - Overlays under
services.<name>may set onlyenabled:,ports:, andhosts:. Any other field there is a layer-aware overlay error — structural fields (container,dir,configs,compose,extends, …) belong inworkspace/services/<name>/service.yml. The overlay validator also enforces shape:ports:must be a map of name → integer in1..65535;hosts:must be a map of name → string. ports:andhosts:are deep-merged by entry name on top of the declared map: a per-developer override underworkspace/local.ymlonly touches the listed keys; declared entries the overlay does not mention are preserved. New entries may also be introduced via overlay. This is a first-class DWE feature: developers routinely need to remap a port that clashes with something already bound on their host, or switch their*.localhostname, without editing the sharedworkspace/services/<name>/service.yml.- Each resolved service (including the post-overlay
ports/hostsnested maps) is injected intoDweConfig.Raw["services"]so dot-paths likeservices.main.ports.httpandservices.adminer.hosts.webresolve in export rules,docker.ymltemplates, commanddefault_from:, andinfo.ymlreferences. - Port values are bounded
1..65535at load time (both inservice.ymland in overlay layers).
Example: a developer whose host already binds 8027 remaps adminer locally without touching the shared config —
# workspace/local.yml (not tracked by git)services: adminer: ports: http: 9027 # overrides declared 8027 main: hosts: api: api.dev.local # adds a new entry; web stays as declaredStructure
Section titled “Structure”Each service lives in its own folder under workspace/services/. The folder name becomes the service key.
workspace/services/ main/ service.yml deploy.yml # optional; enables deploy lifecycle for this service db/ service.yml varnish/ service.yml adminer/ service.yml# type: app — owns source under dir:, has deploy lifecycle, renders templatestype: appcontainer: app-mainrequired: truedir: ./services/maindir_internal: /workspacework_dir_internal: /workspace/srcextends: <parent-app-key> # app-onlydepends_on: [db, redis] # may target app or infra (never tool)icon: "📦"hosts: web: app.localhostports: http: 80info: title: "Main Application" primary_host: web primary_port: http paths: - name: "API Documentation" path: /api/docs icon: "📖"compose: - compose/services/main/overlay.ymlconfigs: - file: .env mountpoint: src/.envdirs: [logs, home, runtime]cli: mode: auto|exec|run shell: bash user: www-data workdir: /workspace/src env: - KEY=valuerender: ide: { enabled: true, template: <pack> } ai: { enabled: true, template: <pack> } git: { enabled: true, template: <pack> }# type: infra — backing service, may be a depends_on targettype: infracontainer: dbrequired: true # always-on backing serviceports: mysql: 13306# type: infra (optional) — toggleable via `dwe services enable varnish`# Note: container field is omitted here — it defaults to the folder name "varnish"type: infracompose: - compose/services/varnish/overlay.ymlports: http: 6081# type: tool — ephemeral utility container, never a depends_on targettype: toolcontainer: adminericon: "🔧"compose: - compose/tools/adminer.ymlports: http: 8027hosts: web: db.localhostinfo: title: Adminer- Field reference — every top-level field plus the
ports,hosts,icon,info,configs,dirs,cli,status, andrenderblocks - Inheritance via
extends— toposort, resolution rules, app-only guard, worked example - Examples and toggle lifecycle — full service definition,
on_enable/on_disable/notes, common pitfalls
Related commands
Section titled “Related commands”dwe shell [service]— open a shell in any enabled service container (thecli:defaults block istype: apponly; tool/infra use built-in bash/auto defaults).dwe status— composite read-only view: apps + tools + infra sections, each with customstatus:columns.dwe status apps/dwe status tools/dwe status infra— per-type tables.dwe services— interactive multi-select toggle for every optional service across all types.dwe services list— read-only listing of every configured service (apps, tools, infra, including required infra) with its enabled/running status; the same view baredwe servicesfalls back to on a non-interactive stdin or under--output json. Never writeslocal.yml, never runs lifecycle hooks.dwe services enable <name>/dwe services disable <name>— toggle by name (type looked up internally).dwe deploy run— runs the full deploy pipeline; enumerates all enabled services that have aworkspace/services/<name>/deploy.yml(any service type).dwe reset run --service <name>— resets a single service: stops and removes the container, deletes the servicedir:if declared and present, runs per-servicereset.ymlif present, marks service as requiring a subsequent deploy. Volumes are not auto-removed (opt in viadocker_remove_project_volumes).