Docker Compose Override
dde configures Docker Compose projects through two complementary mechanisms:
- Persistent modifications to the project’s
docker-compose.ymlduringproject:init(viaDockerComposeModifier) - A temporary runtime override file generated during
project:up(viaDockerComposeManager::generateOverride())
The split is deliberate: everything that is pure dde infrastructure (networks, SSH-Agent socket) lives only in the runtime overlay, so the committed compose file stays usable outside of dde. Only project-specific values that a developer would reasonably want to read or tweak in source control — Traefik labels, DATABASE_URL, MAILER_DSN — are written back to docker-compose.yml.
Persistent Compose Modifications (project:init)
Section titled “Persistent Compose Modifications (project:init)”The DockerComposeModifier class modifies the project’s actual docker-compose.yml during project:init. These are one-time changes committed to the repository.
1. Remove v1 Network Boilerplate
Section titled “1. Remove v1 Network Boilerplate”If the compose file still carries the v1-era networks: default: name: dde, external: true block, it is stripped:
# before project:initnetworks: default: name: dde external: trueIn v2, the per-project dde-services-<project> (main) or dde-services-<project>-<suffix> (worktree) network is injected by the runtime overlay, not declared in the committed file — and it is created unconditionally, even for projects that declare no services: in .dde/config.yml. The shared dde network is never used for project containers. DockerComposeModifier::removeDdeNetworkBoilerplate() handles the cleanup of v1-era network declarations.
2. Traefik Labels
Section titled “2. Traefik Labels”Routing labels are added to web services so Traefik can route HTTP/HTTPS traffic:
services: web: labels: - 'traefik.enable=true' - 'traefik.http.routers.myproject-test-web.rule=Host(`myproject.test`)' - 'traefik.http.routers.myproject-test-web-tls.rule=Host(`myproject.test`)' - 'traefik.http.routers.myproject-test-web-tls.tls=true'If the compose file contains VIRTUAL_HOST and/or VIRTUAL_PORT environment variables (v1 format), they are consumed and converted to Traefik labels. The environment variables are then removed.
If a VIRTUAL_PORT is specified, a load balancer port label is also added:
- 'traefik.http.services.myproject-test-web.loadbalancer.server.port=8080'Generated by DockerComposeModifier::addTraefikLabels().
3. Remove v1 SSH-Agent Boilerplate
Section titled “3. Remove v1 SSH-Agent Boilerplate”Old compose files from v1 declared the SSH-Agent socket volume and the SSH_AUTH_SOCK environment variable directly on every service. DockerComposeModifier::removeSshAgentBoilerplate() strips the service-level volume mount, the SSH_AUTH_SOCK env, and any top-level dde_ssh-agent_socket-dir (or legacy v1-named) volume declaration. In v2 all SSH-Agent wiring is injected by the runtime overlay instead.
4. Database Environment Variables
Section titled “4. Database Environment Variables”When the compose file defines a database service (mariadb or postgres), the modifier automatically adds DATABASE_URL to the main container:
services: web: environment: - DATABASE_URL=mysql://root:root@mariadb:3306/myprojectSimilarly, if a mailpit service is present:
services: web: environment: - MAILER_DSN=smtp://mailpit:1025These are only added if the variables are not already defined in the service environment or in the project’s .env / .env.dev files. Generated by DockerComposeModifier::addServiceEnvironment().
5. V1 Build Args Removal
Section titled “5. V1 Build Args Removal”Legacy DDE_UID and DDE_GID build args from v1 are cleaned up by DockerComposeModifier::removeV1BuildArgs().
Runtime Override (project:up)
Section titled “Runtime Override (project:up)”During project:up, DockerComposeManager::generateOverride() creates a temporary override file that layers runtime configuration on top of the project’s compose file without modifying it.
1. Entrypoint and Adapters Mount
Section titled “1. Entrypoint and Adapters Mount”The override replaces the entrypoint with the dde entrypoint and mounts adapter scripts:
services: web: entrypoint: ['/dde/entrypoint.sh'] command: ['/original/entrypoint.sh', 'original-cmd'] volumes: - /path/to/resources/entrypoint.sh:/dde/entrypoint.sh:ro - /path/to/resources/adapters:/dde/adapters:roThe original entrypoint and CMD from the Docker image are preserved as the command, so exec "$@" in the dde entrypoint chains to the original startup sequence.
If the project has custom adapters in .dde/adapters/, they are also mounted:
- /path/to/project/.dde/adapters:/dde/adapters-project:ro2. Environment Variables
Section titled “2. Environment Variables”The override passes the host user context, the SSH agent socket path, and the optional shell preference:
services: web: environment: DDE_UID: "1000" DDE_GID: "1000" DDE_SHELL: "zsh" SSH_AUTH_SOCK: "/tmp/ssh-agent/socket"SSH_AUTH_SOCK is only injected for services whose image has a shell (see DockerManager::imageHasShell()). Shell-less images (e.g. scratch or distroless) receive only the labels + networks and are left otherwise untouched.
3. Networks
Section titled “3. Networks”The overlay declares the per-project network as external and attaches every service to it. The per-service networks: field is emitted with the !override YAML tag so Compose replaces (not merges) any networks the base file declared on that service — a legacy networks: [dde] left over from a v1 layout would otherwise survive the merge and reintroduce the cross-checkout DNS collision the isolation work prevents. A traefik.docker.network label pins Traefik’s lookup network so it can resolve upstream IPs:
networks: dde-services-myproject: external: true
services: web: labels: - 'traefik.docker.network=dde-services-myproject' networks: !override dde-services-myproject: nullProject containers join exactly one network: the per-project one. Extra networks (an integration with an external Docker network outside the dde stack) are not supported on the base compose path — wire them up post-up via a hook or docker network connect if needed.
dde-services-<project> (main checkout) or dde-services-<project>-<suffix> (worktree) is the per-project network where the versioned service containers (mariadb, postgres, …) are reachable under their canonical names. The worktree variant lets a branch run a different service version than main without alias collisions.
Project containers never join the shared dde network — they only join their per-project network. Two checkouts of the same project (main + worktree) would otherwise both register identical service aliases on dde, and Docker DNS would round-robin between them — randomly routing cross-container calls to the wrong checkout. To keep inbound HTTP routing working, ProjectLifecycleManager::ensureProjectNetwork() attaches Traefik to the per-project network on project:up and project:down detaches it again.
The per-project network is created on every project:up, regardless of whether .dde/config.yml declares any services:. That keeps the network rule one-line — “one per-project network, period” — and removes a special-case fallback in both code and docs.
When a project switches a service version (e.g. mariadb 11.8 → 10.11), project:up detaches the previously attached dde-<service>-* container of the same service type before connecting the new one. Without this cleanup the canonical alias (mariadb) would resolve to two containers and Docker DNS would round-robin between them, randomly routing application traffic to the wrong database. Containers of unrelated service types and project app containers are left untouched.
4. SSH-Agent Volume
Section titled “4. SSH-Agent Volume”For every shell-bearing service, the override mounts the shared SSH-Agent socket directory and declares the backing volume at the top level:
services: web: volumes: - dde_ssh-agent_socket-dir:/tmp/ssh-agent:ro
volumes: dde_ssh-agent_socket-dir: external: truePaired with SSH_AUTH_SOCK=/tmp/ssh-agent/socket (see above), this gives container processes transparent access to the host SSH keys without any wiring in the project’s compose file.
5. Dev Layer Image Override
Section titled “5. Dev Layer Image Override”If a dev layer was built (see Dev Layer Builder), the override replaces the service image with the dev layer image tag:
services: web: image: dde-myproject:dev6. Management Label
Section titled “6. Management Label”All services get a dde.managed=true label for identification:
services: web: labels: - 'dde.managed=true'7. Worktree Traefik Label Override
Section titled “7. Worktree Traefik Label Override”When running in a git worktree, additional Traefik router labels are generated for the worktree hostname (e.g. feature.myproject.test).
8. Worktree extra_hosts Rewrite
Section titled “8. Worktree extra_hosts Rewrite”In a worktree, extra_hosts entries whose hostname is <project>.test or a subdomain thereof are re-emitted with the worktree-hostname variant, tagged !override so Compose replaces (not merges) the base list on the worktree container. Inside containers, dde’s host-side dnsmasq is unreachable — /etc/hosts (populated from extra_hosts) is the only resolver for <project>.test, so without the rewrite a worktree’s container would hold the main checkout’s hostnames and could not reach its own worktree URLs.
Generated by DockerComposeManager::generateOverride() via WorktreeManager::rewriteExtraHosts(). The override is only emitted when at least one base entry references the project host; otherwise the base list passes through unchanged.
Override Lifecycle
Section titled “Override Lifecycle”The runtime override file is:
- Generated before
docker compose upbyDockerComposeManager::generateOverride() - Applied as the last
-fargument so it has the final word on runtime-critical fields (network, worktree hostname, Traefik labels) - Removed after
docker compose upcompletes (in afinallyblock to ensure cleanup)
This approach keeps the project’s compose file unmodified at runtime.
User-Supplied docker-compose.override.yml
Section titled “User-Supplied docker-compose.override.yml”A project may ship its own override file alongside the base compose file. Docker Compose normally auto-merges it on docker compose up, but that auto-discovery is disabled the moment dde passes explicit -f arguments. DockerComposeManager::findUserOverrideFile() detects the override and ProjectLifecycleManager::up() slots it in between the base file and the dde-generated overlay:
docker compose \ -f docker-compose.yml \ -f docker-compose.override.yml # user override — optional -f /tmp/dde-override-… # dde runtime overlay — last word up -dThe override is paired by base filename — compose.yml looks for compose.override.yml (or .yaml), docker-compose.yml looks for docker-compose.override.yml (or .yaml). A project on compose.yml will not silently pick up a stray docker-compose.override.yml.
Typical use cases:
- Developer-local tweaks (
DISPLAYfor a Playwright container, host port bindings) - Optional debug services that should not ship in the committed base file
The dde overlay always wins for fields it sets explicitly (worktree hostname, per-project network, Traefik routing for known hosts, and — in worktrees only — the extra_hosts rewrite for project hostnames). For everything else — environment, volumes, additional services, and extra_hosts outside a worktree — the user override behaves exactly as it would without dde.