Skip to main content
Self-Hosting & Privacy

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.

Milan BuhaSeptember 30, 20266 min read
ShareXin
Docker Compose Profiles Explained

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 no profiles: key always start.
  • docker compose --profile monitoring up activates a named group; you can pass --profile more than once to combine groups.
  • Splitting an 8-service stack into core / monitoring / backup profiles cut a full restart's RAM footprint and boot time roughly in half β€” the numbers are below.
  • COMPOSE_PROFILES in a .env file sets a default so you don't have to type --profile every time locally.
  • A service left without a profiles: key by mistake will start on every single up, 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 up doesn't error; it just starts nothing extra, because no service matches a profile that doesn't exist.
  • depends_on reaching across profile boundaries. If app-a (always-on) has depends_on: [uptime-kuma] (profile-gated), a bare up tries to start app-a without ever starting the dependency it named β€” see our Docker Compose networking guide if that dependency is also how services reach each other. Keep depends_on inside the same profile group it's declared from.
  • Assuming down respects profiles the same way up does. docker compose down without a matching --profile flag 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.

Related stories

More from Self-Hosting & Privacy

Stay in the loop

Get the latest articles delivered to your inbox. No spam, unsubscribe anytime.

Read next

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