One file with eleven services, of which four run in production, three run locally and four run in both with different settings.
compose.yaml the shared definition. no
ports, no volumes, no
environment beyond defaults.
compose.override.yaml local. bind mounts, exposed
ports, mailpit, xdebug.
loaded automatically.
compose.prod.yaml production. resource limits,
restart policies, the log
driver, no build sections.
$ docker compose -f compose.yaml -f compose.prod.yaml up -d
The override file being loaded automatically is what makes the split ergonomic locally and is also the trap — a production command that forgets to name its file gets the local overrides silently. The deploy script names both files explicitly and the local one names neither, which is the arrangement that fails safely in the direction that matters.