codingstairs
NotesEDULifeContact
⌕Search⌘K
koen

Navigation

  • Intro
  • Blog
  • Life

Get in touch

Send without signing in. Add your email if you'd like a reply.

  • Leave a message anonymously →
  • ✉ warragon112@gmail.com
  • KakaoTalk Open Chat ↗

© 2026 codingstairs

  • Notes
  • EDU
  • Search
  • Life
  • Contact
  • Legal
  • RSS
  • GitHub
Notes›infra

Docker Compose Patterns

Published 2026-04-28· Updated 2026-05-18·0 views

Docker Compose Patterns

When running several containers on a single host (web, DB, cache, queue) Compose is the lightest path. One YAML file ties together dependencies, networks, volumes, and healthchecks. Not being a full orchestrator is both its strength and its limit. This piece covers the v1 vs v2 difference, the core keys of compose.yaml, healthcheck, env_file, profiles, and override patterns.

1. About Compose

Compose started as an external tool called Fig (2014); Docker acquired and absorbed it. v1 was a separate Python CLI (docker-compose), and from v2 (2021) it was rewritten in Go and merged into the docker compose subcommand. v2 differs in BuildKit support, profiles, and faster execution.

The recommended file name is compose.yaml or compose.yml. The older convention docker-compose.yml is still recognized.

2. Basic Structure

services:
  web:
    build: ./web
    ports:
      - "127.0.0.1:8080:8080"
    environment:
      DATABASE_URL: postgres://app:${DB_PASS}@db:5432/app
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: ${DB_PASS}
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5

volumes:
  db-data:

3. Core Keys

Key Role
services Container-level definitions.
volumes Named volumes.
networks User-defined networks (a default is auto-created if omitted).
secrets Secret value mount (file or external secret).
configs Config file mount.
profiles Optional service grouping.

4. Dependencies and Healthcheck

depends_on alone is a weak guarantee that "another service is alive." Even when the network is up, the DB may not be ready to take queries. In v2, combine condition: service_healthy with healthcheck to verify true readiness.

5. env_file and ${VAR} Substitution

services:
  web:
    env_file: .env
    environment:
      LOG_LEVEL: ${LOG_LEVEL:-info}

env_file injects environment variables into the container. ${VAR} substitution is read by Compose from the .env file and the shell environment, filling in the YAML itself. The two have different meanings — one is the container environment, the other is text substitution in the Compose file.

6. profiles

Attach a profile to services that should only come up in certain environments:

services:
  app: { ... }
  pgadmin:
    image: dpage/pgadmin4
    profiles: ["dev"]

docker compose --profile dev up brings up the dev-profile services as well. Useful for naturally excluding tooling that's not needed in production.

7. Override File

The compose.override.yaml next to compose.yaml is auto-merged. For environment-specific branches it's clearer to use explicit files:

docker compose -f compose.yaml -f compose.prod.yaml up -d

Later files override earlier ones. Splitting dev / staging / prod differences into small patch files is a common pattern.

8. extends

Inherit and compose service definitions from another file. The right place to define a shared base in a large monorepo:

services:
  app:
    extends:
      file: common.yaml
      service: app-base

9. Other Paths

The comparisons to Compose are limited to single-host setups.

  • systemd unit + Docker — service definitions managed by systemd. The dependency graph is systemd's After= and Requires=.
  • Podman + quadlet — Podman's systemd integration. Container definitions in *.container files.
  • Nomad (HashiCorp) — a lightweight single-binary orchestrator. Single host up to hundreds of nodes.
  • Kubernetes — a different weight class. Often considered overkill on a single host.

The moment you spread across multiple hosts, Compose isn't enough. Move on to Swarm, Nomad, or k8s.

10. Single-host Suitability

Strengths — small learning cost, every definition in one file, fast start and stop.

Limits — single host (no horizontal scaling). Zero-downtime deploys need an external tool or a manual procedure. Secret management and rolling upgrades are weak.

For most side projects and small-to-mid services, a single host plus Compose is enough.

11. Common Patterns

Build and image side by side:

services:
  app:
    image: ghcr.io/my/app:1.4.2
    build: ./app

docker compose build then push in CI. The production host runs pull. Putting image and build together lets the two flows merge naturally.

Logging and restart — keep the default log driver from filling the disk:

services:
  app:
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "5"

A YAML anchor for shared logging — instead of repeating the same logging: block on every service, define it once via an anchor and reference it:

x-logging: &default-logging
  driver: json-file
  options:
    max-size: "10m"
    max-file: "3"

services:
  web:
    logging: *default-logging
  db:
    logging: *default-logging

The x- prefix is Compose's "extension field" convention — Compose ignores it but the YAML parser recognizes the anchor. Single point of policy change. Since the default driver accumulates without limit when nothing is set, you'll end up writing the same block on every service anyway — start with the anchor.

12. Common Pitfalls

Missing bind address on a port — "8080:8080" binds to 0.0.0.0 and is reachable from outside. In production security models, "127.0.0.1:8080:8080" is often the right call.

Believing depends_on alone is enough — without condition, only the start order is guaranteed.

Volume vs bind mount permissions — when the host user (uid) and container user differ, write failures show up frequently.

Security of .env — stored in plaintext for Compose's ${VAR} substitution. Keep secrets in a separate tool (Docker secret, age, sops).

Name collisions — running multiple Compose projects on one host with the same project name mixes containers and networks. Use --project-name or separate directories.

Closing thoughts

Compose is the most intuitive tool for tying every concern of a single host into one YAML. With healthcheck plus condition: service_healthy, 127.0.0.1 binding, and override-file branching all in place, operational stability improves significantly.

Next

  • caddy-not-nginx
  • loopback-ssh-tunnel

Refer to the Compose docs, the Compose Specification, the Compose file reference, Healthcheck, Profiles, and Podman + quadlet.

More in infra

All in this category →
  • Cloud Emulator Stack — Designing a 4th Environment
  • Local HTTPS — mkcert and a Self CA
  • The Place of Single-server Operations
  • Loopback Binding and SSH Tunnel
  • Caddy and nginx — A Comparison
  • Docker Basics