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
- 2. Structuur van je repository
- 3. Compose-bestand: versie, naamgeving en consistentie
- 4. Omgevingsvariabelen:
.envenenv_filecorrect gebruiken - 5. Secrets: voorkom dat wachtwoorden in Compose belanden
- 6. Netwerken: segmentatie, interne netwerken en poorten
- 7. Volumes: persistentie, permissies en back-ups
- 8. Healthchecks en afhankelijkheden: betrouwbaar opstarten
- 9. Resource-limieten en stabiliteit
- 10. Logging en observability
- 11. Reverse proxy en TLS: Traefik als voorbeeld
- 12. Zero-downtime-achtige updates en rollback-strategieën
- 13. Profielen en omgevingen: dev, staging, productie
- 14. Beveiliging: least privilege, read-only en capabilities
- 15. Praktisch voorbeeld: volledige stack
- 16. Dagelijkse beheercommando’s
- 17. Veelgemaakte fouten en hoe je ze voorkomt
1. Voorbereiding en uitgangspunten
Vereisten
- Docker Engine en de Compose-plugin (moderne aanpak:
docker compose, nietdocker-compose) - Basiskennis van containers, images en volumes
- Een Linux-host is het meest gebruikelijk voor productie, maar de principes gelden overal
Controleer je versies:
docker version
docker compose version
Belangrijke ontwerpkeuzes
-
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. -
Configuratie buiten de image
Gebruik omgevingsvariabelen, config-bestanden als bind mounts of secrets. -
Herhaalbaarheid
Een deploy moet op elke host hetzelfde gedrag geven, mits dezelfde inputs (images, env, secrets). -
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:
compose.ymlbevat de basis (geschikt voor productie).compose.override.ymlis voor lokale ontwikkeling (extra poorten, live reload, debug).- Secrets staan niet in git. Voeg
secrets/toe aan.gitignore.
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:
- Gebruik duidelijke servicenamen:
app,db,redis,proxy - Gebruik expliciete images met tags (liefst pinnen op minor/patch)
- Vermijd
latestin productie
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:
- Gebruik
.envvoor Compose-waarden (tags, projectnaam, poorten). - Gebruik
env_filevoor runtime-config van de applicatie. - Zet secrets niet in
env_fileals je kunt vermijden; gebruik secrets.
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.
frontend: verkeer van buiten naar reverse proxybackend: intern verkeer tussen app en databaseinternal: truewaar mogelijk
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:
- Publiceer alleen poorten die extern bereikbaar moeten zijn (meestal alleen de proxy).
- Laat databasepoorten intern; beheer via
docker execof een tijdelijke admin-container.
7. Volumes: persistentie, permissies en back-ups
Named volumes versus bind mounts
- Named volume: beheerd door Docker, stabieler voor productie.
- Bind mount: handig voor development (broncode live), maar productiegevoelig (paden, permissies).
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:
- Maak een user in je Dockerfile
- Zet
USERin de image - Gebruik in Compose zo min mogelijk
user:overrides
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:
- In klassieke Docker Compose (zonder Swarm) worden
deploy:-limieten niet altijd afgedwongen zoals je verwacht. Test dit op jouw platform. - Als je harde limieten nodig hebt, overweeg cgroups-instellingen via Docker run-opties, of orkestratie die dit consistent afdwingt.
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:
- Traefik publiceert 80/443
- App draait alleen intern
- Routing via labels
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:
- Mount de Docker socket read-only.
- Zet
exposedbydefault=falsezodat je expliciet moet opt-innen. - Publiceer het dashboard niet publiek; zet het achter auth of alleen intern.
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.
- Zet in
.envde vorige tag:sed -i 's#APP_IMAGE=.*#APP_IMAGE=ghcr.io/voorbeeld/app:1.4.1#' .env - 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:
- Houd productie zo minimaal mogelijk.
- Dev krijgt extra tools, debugpoorten, bind mounts.
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:
- Traefik als reverse proxy
- Een webapp
- Postgres
- Redis
- Interne netwerken
- Secrets
- Healthchecks
- Log-rotatie
- Beperkte exposure van poorten
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:
- Je database is niet direct vanaf buiten bereikbaar, want er is geen
ports:opdb. - Alleen
proxypubliceert 80/443.
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
-
latestgebruiken in productie
Gevolg: onvoorspelbare updates. Gebruik vaste tags zoals1.4.2. -
Secrets in
.envof in git
Gebruik secrets-bestanden en zorg dat ze niet gecommit worden. -
Databasepoorten publiceren
Publiceer alleen de reverse proxy. Houd databases intern. -
Geen healthchecks
Zonder healthchecks isdepends_ononvoldoende en krijg je instabiele opstart. -
Geen log-rotatie
JSON-logs kunnen je schijf vullen. Stelmax-sizeenmax-filein. -
Bind mounts in productie zonder plan
Bind mounts zijn gevoelig voor hostpadverschillen en permissies. Gebruik named volumes waar mogelijk. -
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).