Docker integration
How DWE drives Docker Compose: the compose project name, the file list it assembles, the environment it propagates to every child process, the volumes it owns, and the few places it bypasses compose and calls docker stop / docker rm directly.
Contents
Section titled “Contents”- Two entry points:
dwe dockervsdwe compose - Project name
- Compose file list
- Process environment
- Volumes
- Compose-bypass on the per-service path
dwe deployend-to-end- Where to go next
Two entry points: dwe docker vs dwe compose
Section titled “Two entry points: dwe docker vs dwe compose”DWE never asks the user to type docker compose directly. Every lifecycle invocation routes through one of two CLI surfaces:
| Surface | Purpose | Policy args applied? |
|---|---|---|
dwe docker <sub> | Public lifecycle API used by Makefiles, deploy steps, and user commands. | Yes (global + per-subcommand defaults). |
dwe compose raw <args...> | Low-level diagnostic pass-through. | No. |
dwe compose files / compose argv | Inspect the active file list or the full effective argv. | n/a (read-only). |
Both surfaces resolve through the same underlying argv assembler and produce two flavours of compose invocation:
- Public lifecycle calls assemble
compose -p <project> -f <file>… <globalArgs> <command> <commandDefaultArgs> <extraArgs>. This is whatdwe dockerand pipelinedocker.*builtins use. - Internal probes (health checks, “is container running” queries) build the same skeleton but skip both
globalArgsand per-command defaults, so a user-supplied override likeargs.ps: ["--services"]cannot break them.
Project name
Section titled “Project name”The compose project name is the -p <name> value passed to every docker compose invocation. It is also the prefix Docker Compose uses for its own resource naming conventions: containers (<project>-<service>-<n>), networks (<project>_default), and named volumes (<project>_<vol>).
DWE resolves the name from workspace/docker.yml:
project_name: "${project.prefix}-${project.name}"${dot.path} placeholders are resolved against the merged project configuration (the workspace.yml → defaults.yml → local.yml cascade). The same name is fed back into:
- Every compose invocation (
compose -p <name>). - The non-shared volume name resolver (
<project>_<volume>— see Volumes). - The per-service container name resolver used by per-service stop and reset (see Compose-bypass).
- The compose-bypass volume-removal builtin, which uses the project name as a prefix filter when sweeping volumes.
A typical resolved name looks like myorg-shop or dwe-laravel. The exact form is part of the public surface — renaming the project requires updating local.yml so the prefix matches, otherwise the next docker compose invocation talks to a different project and the old containers and volumes go orphan.
Compose file list
Section titled “Compose file list”DWE does not write a single compose.yaml that imports everything. It passes a list of -f flags, in order, and lets Docker Compose merge them. The list is assembled deterministically:
- The base file from
compose.baseinworkspace.yml(always included). - Enabled tool overlays, sorted by service key.
- Enabled infra overlays, sorted by service key.
- Enabled app overlays, sorted by service key.
Service type order matters: tools first, then infra, then apps. Within a group, sort is alphabetical by the service key (the directory name under workspace/services/<name>/). The explicit sort keeps the file list deterministic so docker compose always sees overlays in the same merge order.
dwe docker pull --all and dwe docker build --all operate on the same ordered list but ignore the enabled flag, so a developer can pull or build images for overlays they have toggled off locally — without modifying workspace/local.yml.
Each entry in the list points at a file inside the project tree, typically under compose/:
compose/base.yml # compose.basecompose/tools/redis-insight.yml # tool overlaycompose/services/api.yml # app overlayThere is no docker.local.yml-level override of the compose file list. Local overrides live in:
workspace/local.yml— per-serviceenabled: true|false, ports, hosts, custom envs. Affects the list contents via the enabled set.workspace/docker.local.yml— per-policy overrides (project name, args, process env, topology). Does not add or remove-ffiles.
To inspect the effective list run dwe compose files.
Process environment
Section titled “Process environment”Every docker compose child process inherits the parent process environment plus an overlay defined in workspace/docker.yml:
process_env: DOCKER_CLI_HINTS: "false"Compose.BuildEnv() returns os.Environ() with these keys overlaid — existing values are replaced, new keys are appended, and the result is sorted-stable for deterministic test output. When process_env is empty, BuildEnv() returns nil and the child inherits the parent environment unchanged (the common path).
process_env affects the compose CLI process, not the running containers. Container-visible env comes from .env (and the compose file’s environment: blocks). DWE auto-regenerates .env from the active config before five subcommands — up, run, exec, restart, build. This step is intentionally not configurable. Other subcommands (down, stop, ps, logs, pull) skip it because they do not need a current .env.
Volumes
Section titled “Volumes”Docker Compose creates named volumes lazily: the first docker compose up that references a volume creates it. DWE adds two layers on top:
- Project scoping. Volumes declared under
resources.volumesinworkspace/docker.ymlget a<project_name>_prefix, matching Compose’s own naming convention forvolumes:declared insidecompose.yaml. A volume withname: build_artifactsandshared: falsebecomes the actual Docker volumemyorg-shop_build_artifacts(the prefix applies to thename:field, not the map key). - Shared mode.
shared: trueopts out of the prefix. The volume is created with its literal name and survivesdwe resetruns on this project — and is reused by any other DWE project that declares the same shared name. The canonical use case is a language-toolchain cache (composer, npm, go-build) shared across projects.
ensure_before: [up, deploy] triggers idempotent creation on those entry points. The non-shared, project-scoped volumes are also what docker_remove_project_volumes sweeps during reset: the builtin lists every Docker volume whose name begins with <project_name>_ and removes it. Shared volumes do not match the prefix and survive.
Compose-bypass on the per-service path
Section titled “Compose-bypass on the per-service path”For full-stack lifecycle (dwe run, dwe stop, dwe restart with no service argument) DWE calls docker compose up / down / stop. The compose file list and policy args apply as described above.
Two flows deliberately bypass compose:
dwe stop <service>. When the user names a single service, DWE resolves the compose container name throughdaemon.ResolveContainerName(projectFull, svc.Container)and callsdocker stop <name>directly. This works even after the service has been disabled inlocal.yml— at which point the service’s overlay is no longer in the-flist anddocker compose stop <name>would not see the container at all. The user-visible behavior is “I can always stop this service by name”.dwe reset run --service <name>. The per-service reset prepends a syntheticdocker_stop_remove_containerbuiltin step to the service’s pipeline. The builtin stops and removes the named container in twodockercalls, again outside compose. The reset pipeline body then runs as declared; volume cleanup happens only when the user opts in viadocker_remove_project_volumes.
For everything else — dwe docker up, down, logs, ps, exec, run, pull, build, plus dwe stop with no service argument — DWE talks to docker compose.
dwe deploy end-to-end
Section titled “dwe deploy end-to-end”A dwe deploy run invocation walks the deploy pipeline (preflight → orchestrator phases → per-service overlays → infra after: → final hooks). At several points the pipeline calls into the Docker layer described above:
sequenceDiagram autonumber participant U as User participant CLI as DWE CLI participant Pipe as deploy pipeline participant Env as .env render participant Compose as compose layer participant Docker as docker compose U->>CLI: dwe deploy run CLI->>CLI: preflight + acquire project locks CLI->>Pipe: run phases Pipe->>Compose: resolve project name + file list + process env Note over Compose: stable for the<br/>whole pipeline run Pipe->>Pipe: docker_remove_project_volumes (if declared) Pipe->>Env: regenerate .env before up Pipe->>Compose: assemble up argv (svc...) Compose->>Docker: docker compose -p <project> -f ... up -d --remove-orphans Docker-->>Compose: container IDs Pipe->>Compose: assemble internal ps argv (--status running --services) Compose->>Docker: docker compose ps --services Docker-->>Pipe: running service names Pipe->>Pipe: docker_wait_healthy (poll health by container ID) Pipe-->>CLI: pipeline complete CLI-->>U: deploy ok
Three properties to notice:
- The compose layer is resolved once per pipeline run from the merged configuration and reused for every invocation. Project name, file list, args, and process env are stable across the whole deploy.
- Lifecycle commands and internal probes share the same project name and file list but assemble argv differently. A user override like
args.ps: ["--services"]cannot break the running-services probe because the probe skips user-supplied policy args. .envregeneration runs before the compose call, never in parallel with it. The compose invocation always sees a current.env.
Where to go next
Section titled “Where to go next”docker.ymlfield reference — every field ofworkspace/docker.ymlandworkspace/docker.local.yml.- Render env — what goes into
.envand how${...}is resolved. - Deploy — the pipeline that wraps these compose calls.
- State and locks — why
deploy.lockandsnapshot.lockserialise the pipeline above. - Project layout — where the
compose/overlays live on disk.