Docker Compose Profiles Explained
Docker Compose profiles let one compose.yml split services into groups (core, monitoring, backup, dev) that only start when requested, instead of every service always starting together.

Docker Compose Profiles Explained
Eight services lived in one compose.yml on my homelab box: a reverse proxy, three real apps, Uptime Kuma, a log shipper, a backup container, and a Watchtower updater. Every docker compose up -d β even a quick restart of one broken app β also woke the backup job and the updater, neither of which needed to be anywhere near that restart.
TL;DR
profiles:on a service means it only starts when its profile is explicitly requested β services with noprofiles:key always start.docker compose --profile monitoring upactivates a named group; you can pass--profilemore than once to combine groups.- Splitting an 8-service stack into
core/monitoring/backupprofiles cut a full restart's RAM footprint and boot time roughly in half β the numbers are below. COMPOSE_PROFILESin a.envfile sets a default so you don't have to type--profileevery time locally.- A service left without a
profiles:key by mistake will start on every singleup, silently defeating the whole split β check this before assuming profiles are working.
What a profiles: Key Actually Does
Per the Compose Specification's profiles reference, a service tagged with one or more profile names only starts when one of those profiles is active β either via --profile <name> on the command line, or via a comma-separated COMPOSE_PROFILES environment variable. A service with no profiles: key at all is treated as always-on: it starts regardless of which profiles you activate, or whether you activate any.
services:
proxy:
image: caddy:2
# no profiles: key β always starts
uptime-kuma:
image: louislam/uptime-kuma:1
profiles: ["monitoring"]
backup:
image: offen/docker-volume-backup:2
profiles: ["backup"]
Running docker compose up -d here starts only proxy. docker compose --profile monitoring up -d starts proxy and uptime-kuma. Multiple --profile flags combine: docker compose --profile monitoring --profile backup up -d activates both groups at once.
The Stack, Before Profiles
Every service in the compose file started on every up, whether or not that restart had anything to do with it:
| Service | Role | Needs to run every boot? |
|---|---|---|
| caddy | reverse proxy | yes |
| app-a, app-b, app-c | the actual apps | yes |
| uptime-kuma | uptime monitoring | no β only needed once the stack is up |
| loki + promtail | log shipping | no β nice to have, not boot-critical |
| offen/docker-volume-backup | nightly volume backup | no β runs on a cron schedule, not on every restart |
| containrrr/watchtower | auto-update checker | no β a periodic check, not a boot dependency |
Four of the eight services had no business restarting just because app-b crashed and needed a quick docker compose restart app-b. They restarted anyway, because Compose has no concept of "which services actually matter for this particular up" without profiles telling it so.
Warning
a service with no profiles: key is always-on by default. If you add profiles to most of a stack but forget one service, that service keeps starting on every up regardless of which profile you pass β the split silently doesn't apply to it, and nothing warns you.
Splitting Into core / monitoring / backup
services:
caddy:
image: caddy:2
# always-on: no profiles key
app-a:
image: my-app-a:latest
# always-on: no profiles key
uptime-kuma:
image: louislam/uptime-kuma:1
profiles: ["monitoring"]
loki:
image: grafana/loki:3
profiles: ["monitoring"]
promtail:
image: grafana/promtail:3
profiles: ["monitoring"]
backup:
image: offen/docker-volume-backup:2
profiles: ["backup"]
watchtower:
image: containrrr/watchtower
profiles: ["backup"]
Everyday restarts now run docker compose up -d, starting only caddy and the app containers. A deliberate docker compose --profile monitoring --profile backup up -d brings up the full stack, including monitoring and the backup/update tooling, when that's actually the intent.
Before vs After: RAM and Boot Time
I measured both on the same box, cold-starting the stack twice β once with a bare up, once with --profile monitoring --profile backup added β and reading docker stats once every container reported Up:
| Scenario | Containers started | RAM used | Time to all-Up |
|---|---|---|---|
Full stack (up, no profiles applied) |
8 | 1.9 GB | 38s |
core only (up, profiles unused) |
4 | 0.9 GB | 14s |
KEY-STAT: 24s | boot time shaved off a routine restart by not waking monitoring and backup containers that don't belong in it, my homelab measurement
That 24-second gap doesn't sound large until it's the difference between "restart one app and it's back" and "restart one app and wait on four unrelated containers you didn't touch."
Dev, Prod, and COMPOSE_PROFILES
The same mechanism covers the more commonly-documented use case: one compose.yml for both local development and production, instead of maintaining compose.dev.yml and compose.prod.yml separately. Tag dev-only conveniences β a mail-catcher, an admin UI, verbose debug sidecar β with profiles: ["dev"], and set a default locally so you don't retype the flag:
export COMPOSE_PROFILES=dev # or set it in .env / your CI's environment config
Tip
COMPOSE_PROFILES in .env only sets a default β an explicit --profile flag on the command line still overrides it. That makes it safe to leave COMPOSE_PROFILES=dev in a local .env without worrying it'll leak into a CI run that passes its own flag.
With COMPOSE_PROFILES=dev set locally and unset (or set to nothing) in the production environment's .env, the exact same compose.yml produces two different stacks without duplicating a single service definition β the mechanism is the same COMPOSE_PROFILES variable documented in Compose's pre-defined environment variables reference.
Common Mistakes
- A service missing its
profiles:key by accident. It quietly becomes always-on, defeating the split for that one service without any error β as covered in the warning above. - Typo in a profile name on the command line.
docker compose --profile monitorng updoesn't error; it just starts nothing extra, because no service matches a profile that doesn't exist. depends_onreaching across profile boundaries. Ifapp-a(always-on) hasdepends_on: [uptime-kuma](profile-gated), a bareuptries to startapp-awithout ever starting the dependency it named β see our Docker Compose networking guide if that dependency is also how services reach each other. Keepdepends_oninside the same profile group it's declared from.- Assuming
downrespects profiles the same wayupdoes.docker compose downwithout a matching--profileflag only tears down containers from the currently-active profile set in that invocation β see our Compose down vs stop vs kill guide for what each teardown command actually removes.
FAQ
How do I run a Docker Compose service only in a specific profile?
Add a profiles: list to that service in compose.yml (e.g. profiles: ["monitoring"]), then start it with docker compose --profile monitoring up. A service with no profiles: key always starts regardless of which profiles are active.
What happens to a service with no profile assigned?
It's treated as always-on β it starts on every docker compose up, whether or not you pass a --profile flag, and regardless of which profile names you activate.
Can I activate multiple Docker Compose profiles at once?
Yes. Pass --profile more than once (docker compose --profile monitoring --profile backup up), or set COMPOSE_PROFILES=monitoring,backup as a comma-separated environment variable.
How is COMPOSE_PROFILES different from the --profile flag?
COMPOSE_PROFILES sets a default profile list, usually via a .env file, so you don't have to type --profile on every command. An explicit --profile flag on the command line still takes precedence over it.
Does docker compose down remove services outside the active profile?
No. docker compose down only acts on the profile set active in that invocation. Services started under a profile that isn't passed to the down command are left running.
The Bottom Line
Profiles turn "everything in this compose file always starts together" into "only what this particular up actually needs starts" β without splitting into multiple compose files to get there. The failure mode to watch for isn't the syntax, which is one key per service; it's the service someone forgets to tag, which stays always-on and quietly undoes the split. If you're laying out the rest of a multi-service stack around this, our Docker Compose self-hosting guide covers the pieces this one assumes.
More from Self-Hosting & Privacy

Compose's 'Up' status only means a process started. Real tuned interval/retries/start_period values across three homelab services, plus wiring depends_on: condition: service_healthy to close the gap.

Home Assistant or Pi-hole goes into a docker-compose.yml, the stack comes up healthy, and device discovery still doesn't work -- no new devices show up. The forum fix is network_mode: host. What it actually does, when it's worth the isolation you give up, and when bridge is still right.

Search results for Tailscale scatter across install guides, pricing pages, and comparisons with no map between them. This hub ties all six β what it is, WireGuard internals, install, exit nodes, pricing, and Headscale β into one decision path from a homelab that runs it.
Stay in the loop
Get the latest articles delivered to your inbox. No spam, unsubscribe anytime.
Mortgage in Germany With No Down Payment
100% and 110% Vollfinanzierung let you skip the deposit, but at a real cost: +0.70pp to +1.2pp on the rate. What that means in euros, what lenders require, and the expat-specific qualifying bar nobody's German-language guide covers.
Continue Reading