CertGrid CertGrid
Best Practices·Docker

Docker Compose Profiles, Overrides and Watch

Three features that turn one Compose file into a workflow: optional services behind profiles, per-machine overrides that merge automatically, and live file sync that updates a running container without a rebuild. Including the profile teardown trap that leaves a network behind.

Docker Compose Guide 28 of 46 Advanced

Tested on the versions above. Build output and timings vary. The merge results and the teardown behaviour are what to match.

One Docker host is all this guide needs. Nothing here depends on a second machine, and the hardware above is modest on purpose - a 2 core, 4 GB VM runs everything in this path.
Server NameIP AddressOSRolesCPURAMHDD
DOCKER01192.168.0.21Ubuntu 26.04 LTSDocker Host2 Core4 GB50 GB

Before you start

  1. Profiles: services that stay out of the way

    A service with a profiles: key is not started by ordinary up. It exists in the file, documented and ready, but it only runs when you ask for its profile. This is where debug tooling, seed jobs and admin consoles belong - in the file, out of the default path.

    bash
    cat compose.yaml  debug:    image: alpine:${TAG}    profiles: ["debug"]docker compose up -ddocker compose ps --servicesapp

    Expected resultOnly app - the profiled service was skipped entirely.

    Success conditiondebug is absent despite being in the file. That is the feature, not a mistake.

  2. Opt in when you need it

    --profile activates the profile for that command. The already-running services are left alone; only the newly enabled one is created.

    bash
    docker compose --profile debug up -d Container cg-cfg-debug-1 Created Container cg-cfg-debug-1 Starteddocker compose ps --servicesappdebug

    Expected resultBoth services now listed.

    Success conditionThe profiled service started without disturbing the others. COMPOSE_PROFILES=debug in the environment does the same thing persistently.

  3. The teardown trap

    Here is the part that bites. down without the profile does not know about the profiled service, so it removes what it can and then fails to remove the network - because something it is not managing is still attached. The message says the resource is in use and does not mention profiles at all.

    bash
    # down without --profile leaves the profiled container behinddocker compose down Container cg-cfg-app-1 Removed Network cg-cfg_default Removing Network cg-cfg_default Resource is still in use

    Expected resultThe network removal failing, and the profiled container still present.

    Verify it worked

    bash Example session
    docker compose ps -a --format "table {{.Service}}\t{{.Status}}"SERVICE   STATUSdebug     Exited (0) 48 seconds ago

    Success conditionYou have reproduced the leak. The orphan is the reason - not a Docker bug.

  4. Tear it down properly

    Pass the same profile to down that you passed to up. Anything that turns a service on must also turn it off, which is a good argument for putting the profile in COMPOSE_PROFILES rather than remembering a flag on every command.

    bash
    docker compose --profile debug down Container cg-cfg-debug-1 Removing Container cg-cfg-debug-1 Removed Network cg-cfg_default Removing Network cg-cfg_default Removed

    Expected resultThe network removed cleanly this time.

    Verify it worked

    bash
    docker compose ps -a --services# empty - nothing left

    Success conditionNothing is left. docker compose down --remove-orphans is the escape hatch when you have already lost track.

  5. Override files merge automatically

    If a compose.override.yaml sits beside compose.yaml, Compose reads both and merges them, with the override winning. Nothing needs to be passed on the command line. This is how a shared base file survives contact with per-developer differences - the base is committed, the override is ignored by git.

    bash
    cat compose.override.yamlservices:  app:    environment:      GREETING: overridden-locallydocker compose up -d && docker compose logs appapp-1  | app started Network cg-pw_default Creating Network cg-pw_default Creating Network cg-pw_default Created Network cg-pw_default Created Container cg-pw-app-1 Creating Container cg-pw-app-1 Created Container cg-pw-app-1 Starting Container cg-pw-app-1 Started

    Expected resultThe override's value winning over the base file's.

    Success conditionoverridden-locally appears without any extra flags. Note the unrelated extra value is untouched - the merge is per key, not wholesale replacement.

  6. Opt out of the override when you need the base

    Naming files explicitly with -f replaces the automatic discovery, so -f compose.yaml alone reads the base and ignores the override. This is what CI should do - otherwise a developer's stray override file changes the build.

    bash
    docker compose -f compose.yaml up -d --force-recreatedocker compose -f compose.yaml logs appapp-1  | greeting=from-dotenv extra=from-envfile

    Expected resultThe base value returning.

    Success conditionThe override is bypassed. Multiple -f flags merge left to right, which is how per-environment files are composed deliberately rather than by discovery.

  7. Watch: sync files into a running container

    docker compose watch monitors paths declared under develop.watch and acts on changes. A sync action copies the changed file into the running container without a rebuild or a restart; rebuild rebuilds the image instead, for changes that cannot be hot-copied like a dependency manifest.

    bash
    cat compose.yaml    develop:      watch:        - action: sync          path: ./site          target: /usr/share/nginx/html

    Expected resultA watch rule pairing a host path with a container path.

    Success conditionThe rule names both sides. A sync action with no target cannot work out where to put the file.

  8. Watch it happen

    With watch running in one terminal, editing a file on the host updates the container in place. The log line names how many changes it saw, and the served content changes with no rebuild - the container is the same one that was already running.

    bash Example session
    docker compose watch Container cg-pw-app-1 RunningWatch enabled

    Expected resultA sync message, and the edited content served immediately.

    Verify it worked

    bash
    curl -s http://localhost:8099<h1>version two - edited on the host</h1>

    Success conditionThe new content is served without a rebuild. Watch runs in the foreground - it is a development tool, not something to run in production.

Troubleshooting

Official sources