Profiles and Multiple Environments
Not every service should start every time. A database seeder, a debug shell, a one-off migration runner, or an admin UI belongs in the same compose.yaml but should stay dormant unless asked for. Compose profiles tag services so they are excluded from up by default and only start when their profile is activated.
That lets one file describe the full set of optional components without launching all of them on a plain docker compose up. The core stack comes up clean; the extras wait until you name them.
The Problem Profiles Solve
A stack usually has core services that always run — Driftwood's web, db, and proxy — and optional ones you want declared but not started by default: a seed job that loads sample data, a debug container for poking at the database, a DB admin UI. Without profiles you have two bad choices: run them every time, paying for containers you rarely need, or maintain a second compose file and keep the two in sync. Profiles give a third option — one file, with the optional pieces gated.
profiles: on a Service
Listing one or more profile names under a service's profiles: key opts that service out of the default up. A service with no profiles: always runs; a service tagged ["debug"] runs only when the debug profile is active. Tagging is therefore opt-in activation: the absence of a tag means always-on, and the presence of one means dormant until requested. That inversion is the one rule to internalize.
profiles: tag inverts a service's default startupprofiles: key — web, db, proxy. They always start on a bare docker compose up, with no flag needed.seed, debug. They start only when the profile is activated, and sit out of a bare up until then.services:
web:
build: ./web
image: driftwood/web
networks:
- driftwood-net
db:
image: postgres:16
volumes:
- driftwood-db-data:/var/lib/postgresql/data
networks:
- driftwood-net
proxy:
image: nginx:1.27-alpine
ports:
- "80:80"
- "443:443"
networks:
- driftwood-net
seed:
build: ./web
command: ["python", "seed.py"]
profiles: ["seed"]
depends_on:
- db
networks:
- driftwood-net
debug:
image: driftwood/web
command: ["sleep", "infinity"]
profiles: ["debug"]
networks:
- driftwood-net
networks:
driftwood-net:
volumes:
driftwood-db-data:
A bare docker compose up starts web, db, and proxy — the three untagged services — and leaves seed and debug alone. The optional services are fully declared and version-controlled; they just sit out of the default bring-up.
Activating a Profile
docker compose --profile debug up turns the debug profile on for that one invocation, or you set COMPOSE_PROFILES=debug in the environment to the same effect. Multiple profiles can be active at once — --profile seed --profile debug — and naming a profiled service directly on the command line (docker compose run seed) also pulls it in. Activation is per-command, so the file stays stable regardless of who needs what on a given run.
Driftwood's Optional Services
Driftwood's seed service loads sample bookmarks into db and carries the seed profile; its debug service is a shell container on driftwood-net for running psql against the database and is tagged debug. Both stay out of the normal up and start only when a developer asks: docker compose --profile seed up seed to load fixtures, docker compose --profile debug run debug bash to get a shell on the network. The core stack never carries them.
Profiles vs Separate Files
Profiles toggle whole services within one file and are the right tool for optional add-ons to the same stack — present or absent, on or off. What they do not do is vary the configuration of services that are already there. Running the same web with a bind mount in dev and a pinned image in prod is a different job: that is per-environment configuration of shared services, handled by override files in the next topic. Profiles add and remove; override files reshape.
- Tagging a core service like
dbwith a profile and then losing it on a plainup— profiled services do not start by default, sowebcomes up with no database and races into a connection error. - Expecting
docker compose downafter a profiled run to clean up the profiled containers — once started, profiled services are part of the running project and need the profile active (or an explicit target) to be torn down cleanly. - Using profiles to model dev-versus-prod differences in the same service's config — profiles add or remove whole services; per-environment config of shared services is what override files are for, and forcing profiles into that role bloats the file.
- Forgetting that a
depends_ontarget living behind a profile is not pulled in automatically unless that profile is active — the dependency silently stays down and the dependent fails.
- Leave always-on services (
web,db,proxy) untagged and reserveprofiles:for genuinely optional components (seed,debug, admin tooling), so a bareupbrings exactly the core stack. - Activate profiles per invocation with
--profileorCOMPOSE_PROFILESrather than editing the file, keeping the file stable across who needs what on a given run. - Use profiles to toggle whole services and override files (next topic) to vary the configuration of shared services, matching each tool to its job.
- Name profiles by intent —
seed,debug,e2e— so the activation command reads as what it is for rather than as an opaque flag.
docker run workflow models this only by choosing which commands to run
Podman podman-compose supports the same profiles key
Kubernetes no direct equivalent — optional manifests via kustomize overlays or Helm toggles
Knowledge Check
What does adding profiles: ["debug"] to a service do to its default startup?
- It excludes the service from a bare
docker compose up; it starts only whendebugis active - It makes the service start on every
upwith higher scheduling priority than untagged ones - It quietly removes the service from the project network so that it can no longer be reached by name
- It prevents the service's image from ever being built, even when the profile is later activated
How do you start a profiled service, and can more than one profile be active?
- With
--profileorCOMPOSE_PROFILESper invocation, and several profiles can be active at once - By editing the file to remove the
profiles:key from it, and only one profile may ever be active - With a dedicated
docker compose profile upsubcommand, strictly limited to one profile at a time - Compose activates whichever profiles this host has used on a previous run, doing so fully automatically
When should you use override files instead of profiles?
- When you need to vary the configuration of a service that exists in both environments, not add or remove one
- When you want to toggle whether an optional
seedservice runs at all in a given environment - When you want to run two completely unrelated stacks out of one shared project directory simultaneously, isolated from each other
- When you need the services in the stack to resolve each other by name over the project network
Why does tagging db with a profile break a plain docker compose up?
dbbecomes dormant by default, so it does not start andwebraces into a connection error- The profile detaches the
driftwood-db-datavolume, so the database comes up with an empty data dir - The profile removes
dbfrom the project's embedded DNS, so its service name no longer resolves at all - The profile blocks
db's image from pulling from the registry, so the whole stack fails to build
You got correct