← Terug naar tutorials

Applicaties deployen met Docker Compose: geavanceerde best practices

docker composedeploymentcontainersdevopsci/cdzero-downtimesecretshealthchecks

Applicaties deployen met Docker Compose: geavanceerde best practices

Docker Compose is meer dan “een paar containers starten”. In productie-achtige omgevingen wil je herhaalbaarheid, voorspelbaar gedrag bij updates, veilige secrets, duidelijke netwerken, betrouwbare healthchecks, goede logging, en een aanpak die schaalbaar blijft als je stack groeit. Deze tutorial behandelt geavanceerde best practices met realistische voorbeelden en echte commando’s.


Inhoud


1. Voorbereiding en uitgangspunten

Vereisten

Controleer je versies:

docker version
docker compose version

Belangrijke ontwerpkeuzes

  1. Immutable images, mutable data
    Je applicatie draait vanuit een image die je herbouwt bij wijzigingen. Data gaat in volumes of externe diensten, niet in de containerlaag.

  2. Configuratie buiten de image
    Gebruik omgevingsvariabelen, config-bestanden als bind mounts of secrets.

  3. Herhaalbaarheid
    Een deploy moet op elke host hetzelfde gedrag geven, mits dezelfde inputs (images, env, secrets).

  4. Beperkte privileges
    Containers draaien bij voorkeur als niet-root, met minimale capabilities, en zo veel mogelijk read-only.


2. Structuur van je repository

Een schaalbare mappenstructuur helpt enorm:

project/
  compose.yml
  compose.override.yml
  .env
  env/
    prod.env
    staging.env
  secrets/
    db_password.txt
    jwt_secret.txt
  nginx/
    nginx.conf
  app/
    Dockerfile
    ...
  scripts/
    backup-db.sh
    restore-db.sh

Best practice:

Voorbeeld .gitignore:

cat > .gitignore <<'EOF'
.env
secrets/
EOF

3. Compose-bestand: versie, naamgeving en consistentie

Moderne Compose-bestanden hoeven vaak geen version: meer te bevatten. Houd het bestand leesbaar:

Controleer je configuratie:

docker compose config

Dit commando “rendert” je Compose-config en laat zien wat Docker Compose daadwerkelijk gaat gebruiken (handig bij variabelen en overrides).


4. Omgevingsvariabelen: .env en env_file correct gebruiken

.env (Compose-interpolatie)

Het .env-bestand in dezelfde map als je Compose-bestand wordt gebruikt voor variabele-interpolatie in Compose zelf.

Voorbeeld .env:

cat > .env <<'EOF'
COMPOSE_PROJECT_NAME=mijnstack
APP_IMAGE=ghcr.io/voorbeeld/app:1.4.2
APP_PORT=8080
EOF

Gebruik in compose.yml:

services:
  app:
    image: ${APP_IMAGE}
    ports:
      - "${APP_PORT}:8080"

env_file (container-omgeving)

env_file injecteert variabelen in de container, niet in Compose-interpolatie.

services:
  app:
    env_file:
      - ./env/prod.env

Best practice:


5. Secrets: voorkom dat wachtwoorden in Compose belanden

Compose ondersteunt “secrets” vooral goed in combinatie met Swarm, maar ook zonder Swarm kun je veiligere patronen gebruiken:

Optie A: bestanden als secrets-mount (aanbevolen patroon)

Maak een secretbestand:

mkdir -p secrets
chmod 700 secrets
printf 'supergeheimwachtwoord\n' > secrets/db_password.txt
chmod 600 secrets/db_password.txt

Mount als read-only bestand:

services:
  db:
    image: postgres:16.3
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt

Belangrijk: veel officiële images ondersteunen *_FILE varianten (zoals Postgres). Als jouw app dit niet ondersteunt, kun je bij start een entrypoint-script gebruiken dat het bestand leest en een env-variabele exporteert.

Optie B: Docker secrets met Swarm

Als je wél Swarm gebruikt, kun je secrets centraal beheren:

printf 'supergeheimwachtwoord' | docker secret create db_password -

Dan in je stack-deploy. Dit gaat buiten de scope van pure Compose, maar het is een logische vervolgstap voor productie.


6. Netwerken: segmentatie, interne netwerken en poorten

Een veelgemaakte fout is alles op één netwerk en overal poorten publiceren. Best practice: segmentatie.

Voorbeeld:

networks:
  frontend:
  backend:
    internal: true

services:
  proxy:
    image: traefik:v3.1
    networks:
      - frontend
      - backend
    ports:
      - "80:80"
      - "443:443"

  app:
    image: ${APP_IMAGE}
    networks:
      - backend

  db:
    image: postgres:16.3
    networks:
      - backend

Best practice:


7. Volumes: persistentie, permissies en back-ups

Named volumes versus bind mounts

Voor Postgres:

volumes:
  db_data:

services:
  db:
    image: postgres:16.3
    volumes:
      - db_data:/var/lib/postgresql/data

Permissies en gebruikers

Draai services als niet-root waar mogelijk. Sommige images (zoals Postgres) draaien al als eigen user. Voor je eigen app-image:

Back-upstrategie (praktisch)

Een simpele, betrouwbare aanpak: pg_dump in een tijdelijke container die op hetzelfde netwerk zit.

mkdir -p backups
docker compose exec -T db pg_dump -U postgres -d postgres > backups/db_$(date +%F).sql

Restore:

cat backups/db_2026-02-16.sql | docker compose exec -T db psql -U postgres -d postgres

Let op: bij grote databases wil je compressie:

docker compose exec -T db pg_dump -U postgres -d postgres | gzip > backups/db_$(date +%F).sql.gz

8. Healthchecks en afhankelijkheden: betrouwbaar opstarten

depends_on garandeert niet dat een service “klaar” is; alleen dat hij gestart is. Gebruik healthchecks.

Voor Postgres:

services:
  db:
    image: postgres:16.3
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 20

Voor je app kun je een HTTP-check doen:

services:
  app:
    image: ${APP_IMAGE}
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health || exit 1"]
      interval: 10s
      timeout: 3s
      retries: 10

Afhankelijkheid op basis van health:

services:
  app:
    depends_on:
      db:
        condition: service_healthy

Dit voorkomt een groot deel van “race conditions” tijdens deploys.


9. Resource-limieten en stabiliteit

In productie wil je voorkomen dat één container de host uitput.

CPU en geheugen

Compose ondersteunt limieten, maar de exacte werking kan verschillen per omgeving. Een veelgebruikt patroon:

services:
  app:
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
        reservations:
          memory: 256M

Belangrijk:

OOM-gedrag en herstartbeleid

Zet een herstartbeleid:

services:
  app:
    restart: unless-stopped
  db:
    restart: unless-stopped

Voor kritieke services kun je always gebruiken, maar unless-stopped is vaak praktischer bij onderhoud.


10. Logging en observability

Docker logging driver

Standaard logt Docker naar JSON-bestanden. Dit kan schijf vullen. Zet log-rotatie:

services:
  app:
    logging:
      options:
        max-size: "10m"
        max-file: "5"

Controleer logs:

docker compose logs -f --tail=200 app

Metrics en tracing

Compose kan een observability-stack draaien (bijvoorbeeld Prometheus en Grafana), maar dat vraagt extra configuratie. Belangrijkste best practice: expose metrics intern en publiceer ze niet naar buiten.


11. Reverse proxy en TLS: Traefik als voorbeeld

Traefik kan automatisch routes en certificaten beheren. Een beproefd patroon:

Voorbeeld (vereenvoudigd, maar realistisch):

services:
  proxy:
    image: traefik:v3.1
    command:
      - "--api.dashboard=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - frontend
      - backend
    restart: unless-stopped

  app:
    image: ${APP_IMAGE}
    networks:
      - backend
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.app.rule=Host(`app.voorbeeld.nl`)"
      - "traefik.http.routers.app.entrypoints=websecure"
      - "traefik.http.services.app.loadbalancer.server.port=8080"
    restart: unless-stopped

Best practices bij Traefik:


12. Zero-downtime-achtige updates en rollback-strategieën

Docker Compose is geen volledige orkestrator, maar je kunt wel gecontroleerd updaten.

Strategie 1: Rolling-achtige aanpak met meerdere instanties

Compose kan schalen:

docker compose up -d --scale app=2

Als je reverse proxy load balancing doet (Traefik kan dat), kun je updates doen door een nieuwe image te deployen en containers te herstarten. In de praktijk blijft dit beperkt, omdat Compose niet automatisch “rolling” vervangt met health-based drain zoals geavanceerde systemen.

Strategie 2: Blue/green met twee services

Je definieert app_blue en app_green, en switcht routing. Dit vereist wel discipline en routingregels.

Rollback

Rollback is vaak: terug naar vorige image-tag.

  1. Zet in .env de vorige tag:
    sed -i 's#APP_IMAGE=.*#APP_IMAGE=ghcr.io/voorbeeld/app:1.4.1#' .env
  2. Deploy opnieuw:
    docker compose pull
    docker compose up -d

Best practice: tag je images semantisch en bewaar vorige versies.


13. Profielen en omgevingen: dev, staging, productie

Compose-profielen laten je services conditioneel starten.

Voorbeeld: een adminer alleen in dev:

services:
  adminer:
    image: adminer:4.8.1
    profiles: ["dev"]
    ports:
      - "8081:8080"
    networks:
      - backend

Start met profiel:

docker compose --profile dev up -d

Zonder profiel draait adminer niet.

Best practice:


14. Beveiliging: least privilege, read-only en capabilities

Read-only root filesystem

Voor services die geen schrijfrechten nodig hebben:

services:
  app:
    read_only: true
    tmpfs:
      - /tmp

Je moet soms extra paden writable maken (cache, uploads). Gebruik dan een volume of tmpfs gericht op die paden.

Capabilities droppen

Veel containers hebben geen extra Linux capabilities nodig:

services:
  app:
    cap_drop:
      - ALL

Soms heb je specifieke capabilities nodig (bijvoorbeeld bind op lage poorten), maar probeer het minimaal te houden.

Geen privilege escalation

services:
  app:
    security_opt:
      - no-new-privileges:true

Draai als niet-root

Als je image dit ondersteunt:

services:
  app:
    user: "10001:10001"

Let op: dit kan botsen met volume-permissies. Los dit op door volumes correct te initialiseren of door in je Dockerfile ownership te zetten.


15. Praktisch voorbeeld: volledige stack

Hieronder een complete, productiegerichte Compose-config met:

Maak compose.yml:

services:
  proxy:
    image: traefik:v3.1
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--accesslog=true"
      - "--log.level=INFO"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - frontend
      - backend
    restart: unless-stopped
    logging:
      options:
        max-size: "10m"
        max-file: "5"

  app:
    image: ${APP_IMAGE}
    env_file:
      - ./env/prod.env
    environment:
      DATABASE_PASSWORD_FILE: /run/secrets/db_password
      REDIS_URL: redis://redis:6379/0
    secrets:
      - db_password
    networks:
      - backend
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health || exit 1"]
      interval: 10s
      timeout: 3s
      retries: 10
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.app.rule=Host(`app.voorbeeld.nl`)"
      - "traefik.http.routers.app.entrypoints=websecure"
      - "traefik.http.services.app.loadbalancer.server.port=8080"
    restart: unless-stopped
    read_only: true
    tmpfs:
      - /tmp
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    logging:
      options:
        max-size: "10m"
        max-file: "5"

  db:
    image: postgres:16.3
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    volumes:
      - db_data:/var/lib/postgresql/data
    networks:
      - backend
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 5s
      timeout: 3s
      retries: 20
    restart: unless-stopped
    logging:
      options:
        max-size: "10m"
        max-file: "5"

  redis:
    image: redis:7.2.5
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - redis_data:/data
    networks:
      - backend
    restart: unless-stopped
    logging:
      options:
        max-size: "10m"
        max-file: "5"

networks:
  frontend:
  backend:
    internal: true

volumes:
  db_data:
  redis_data:

secrets:
  db_password:
    file: ./secrets/db_password.txt

Maak env/prod.env (zonder secrets):

mkdir -p env
cat > env/prod.env <<'EOF'
DATABASE_HOST=db
DATABASE_NAME=appdb
DATABASE_USER=appuser
PORT=8080
EOF

Maak secretbestand:

mkdir -p secrets
chmod 700 secrets
printf 'kies-een-sterk-wachtwoord\n' > secrets/db_password.txt
chmod 600 secrets/db_password.txt

Deploy:

docker compose pull
docker compose up -d
docker compose ps

Valideer intern netwerkgedrag:


16. Dagelijkse beheercommando’s

Status en processen

docker compose ps
docker compose top

Logs

docker compose logs -f --tail=200
docker compose logs -f --tail=200 app

Shell in een container

docker compose exec app sh
docker compose exec db psql -U appuser -d appdb

Herstarten en herdeployen

docker compose restart app
docker compose up -d

Images bijwerken

docker compose pull
docker compose up -d
docker image prune -f

Let op: docker image prune verwijdert ongebruikte images; doe dit bewust.

Opruimen (voorzichtig)

Stop en verwijder containers, maar behoud volumes:

docker compose down

Stop en verwijder inclusief volumes (data weg):

docker compose down -v

17. Veelgemaakte fouten en hoe je ze voorkomt

  1. latest gebruiken in productie
    Gevolg: onvoorspelbare updates. Gebruik vaste tags zoals 1.4.2.

  2. Secrets in .env of in git
    Gebruik secrets-bestanden en zorg dat ze niet gecommit worden.

  3. Databasepoorten publiceren
    Publiceer alleen de reverse proxy. Houd databases intern.

  4. Geen healthchecks
    Zonder healthchecks is depends_on onvoldoende en krijg je instabiele opstart.

  5. Geen log-rotatie
    JSON-logs kunnen je schijf vullen. Stel max-size en max-file in.

  6. Bind mounts in productie zonder plan
    Bind mounts zijn gevoelig voor hostpadverschillen en permissies. Gebruik named volumes waar mogelijk.

  7. Alles draait als root
    Beperk privileges: no-new-privileges, cap_drop, read_only, en niet-root users.


Afronding

Met deze best practices maak je Docker Compose geschikt voor serieuze deployments: duidelijke scheiding van netwerken, veilige secrets, gecontroleerde updates, betrouwbare healthchecks, en betere beveiliging. Het belangrijkste is consistentie: kies patronen die je team begrijpt, automatiseer waar mogelijk (bijvoorbeeld via een CI-pijplijn die images bouwt en tags beheert), en test je deploy-proces regelmatig alsof je echt moet terugrollen.

Als je wilt, kan ik je huidige compose.yml analyseren en concrete verbeteringen voorstellen op basis van jouw stack (database, proxy, taalframework, hostingplatform).