Production Readiness Checklist voor Dockerized Microservices (DevOps)
Deze tutorial is een praktische, productiegerichte checklist voor Dockerized microservices. Het doel is dat je services voorspelbaar deploybaar, veilig, observeerbaar, schaalbaar en herstelbaar zijn in een echte productieomgeving (Kubernetes, Docker Swarm, ECS, Nomad of “plain” Docker). De voorbeelden gebruiken vooral Docker CLI en generieke Linux-commando’s; waar relevant noem ik ook Kubernetes-equivalenten.
1) Basisprincipes: definieer “production readiness”
Een microservice is “production ready” als je:
- Reproduceerbare builds hebt (zelfde input → zelfde image digest).
- Veilige defaults hebt (least privilege, minimale attack surface).
- Configuratie extern beheert (12-factor: config in env/secret store).
- Health checks hebt (liveness/readiness) en gecontroleerde shutdown.
- Observability hebt (logs, metrics, traces) met correlatie-id’s.
- Capaciteitsgrenzen en resource management hebt (CPU/mem/IO).
- Rollback en roll-forward kunt uitvoeren zonder downtime.
- Incident response kunt doen: debuggen, forensics, audit trails.
- Compliance en supply-chain security op orde hebt.
De rest van dit document is een checklist met uitleg en concrete commando’s.
2) Container image: buildkwaliteit, minimalisme en determinisme
2.1 Gebruik multi-stage builds
Multi-stage builds verminderen image-grootte en attack surface. Voorbeeld (Go), maar conceptueel hetzelfde voor Node/Java/.NET:
docker build -t myservice:local .
docker image ls myservice:local
Controleer lagen en grootte:
docker history myservice:local
docker image inspect myservice:local --format '{{.Size}}'
Waarom dit belangrijk is: kleinere images laden sneller, hebben minder packages (minder CVE’s) en starten sneller bij autoscaling.
2.2 Pin base images op digest, niet alleen tag
Tags zoals alpine:latest zijn veranderlijk. Pin op digest:
docker pull alpine:3.20
docker image inspect alpine:3.20 --format '{{index .RepoDigests 0}}'
Gebruik vervolgens alpine@sha256:... in je Dockerfile. Dit maakt builds reproduceerbaarder en verkleint supply-chain risico’s.
2.3 Minimaliseer packages en verwijder build-tools
Installeer alleen runtime dependencies. Verwijder caches:
- Debian/Ubuntu:
rm -rf /var/lib/apt/lists/* - Alpine:
rm -rf /var/cache/apk/*
Controleer welke binaries aanwezig zijn:
docker run --rm myservice:local sh -lc 'ls -lah /usr/bin | head'
2.4 Maak builds reproduceerbaar
- Vermijd
curl | bashzonder pinning. - Pin versies van dependencies (lockfiles).
- Gebruik
--no-cachein CI wanneer nodig, maar liever: cache met integriteitscontrole.
Voorbeeld: bouw met BuildKit en toon digest:
DOCKER_BUILDKIT=1 docker build -t myservice:ci .
docker image inspect myservice:ci --format '{{.Id}}'
3) Runtime security: least privilege en hardening
3.1 Draai als niet-root
In productie wil je vrijwel nooit root in de container. Controleer:
docker run --rm myservice:local id
Als je uid=0(root) ziet: fixen. In Dockerfile: maak user en switch.
Waarom: bij een exploit is de impact kleiner; bovendien blokkeren veel platforms root.
3.2 Read-only filesystem + tmpfs waar nodig
Veel services hoeven niet naar disk te schrijven. Test read-only:
docker run --rm --read-only myservice:local
Als je app schrijft naar /tmp, mount tmpfs:
docker run --rm --read-only --tmpfs /tmp:rw,noexec,nosuid,size=64m myservice:local
Waarom: beperkt persistence voor attackers en voorkomt dat containers “stiekem” stateful worden.
3.3 Capabilities minimaliseren
Standaard heeft Docker al een set capabilities; minimaliseer verder:
docker run --rm --cap-drop=ALL --cap-add=NET_BIND_SERVICE myservice:local
Alleen toevoegen wat je nodig hebt. Veel HTTP services op poort 8080 hebben zelfs NET_BIND_SERVICE niet nodig.
3.4 Seccomp/AppArmor en no-new-privileges
Controleer of no-new-privileges werkt:
docker run --rm --security-opt no-new-privileges myservice:local
In Kubernetes kun je dit via securityContext afdwingen (conceptueel; details platformafhankelijk).
3.5 Secrets: nooit in images, nooit in logs
- Geen API keys in Dockerfile
ENV. - Geen
.envin image. - Gebruik secret stores (Docker secrets, Kubernetes Secrets, Vault, cloud secret manager).
Test dat je image geen secrets bevat:
docker run --rm myservice:local sh -lc 'grep -R "API_KEY" -n / 2>/dev/null | head'
Dit is geen waterdichte methode, maar kan grove fouten vinden.
4) Configuratiebeheer: 12-factor in praktijk
4.1 Config via environment variables
Zorg dat je service alle runtime config uit env kan halen:
PORT,LOG_LEVEL,DATABASE_URL,REDIS_URL,FEATURE_FLAG_X.
Test lokaal:
docker run --rm -e PORT=8080 -e LOG_LEVEL=info myservice:local
4.2 Validatie bij startup
Een service moet bij start fail-fast als verplichte config ontbreekt. Anders krijg je “half werkende” pods.
Simuleer ontbrekende variabele:
docker run --rm myservice:local
echo $?
Verwacht: non-zero exit code en duidelijke foutmelding.
4.3 Scheid config per omgeving
Gebruik geen if env == prod spaghetti. Gebruik:
- aparte config sets (env vars / secret paths)
- feature flags
- runtime toggles
5) Networking & API: timeouts, retries, idempotency
5.1 Strikte timeouts
Zonder timeouts kunnen threads/connection pools vollopen. Controleer:
- HTTP client timeouts
- DB connect/query timeouts
- upstream deadlines
Test met een “hangende” upstream (voorbeeld met curl naar een niet-routable IP):
time curl -m 2 http://10.255.255.1:1234/ || true
Je service moet vergelijkbare grenzen afdwingen.
5.2 Retries met backoff, maar niet blind
Retries kunnen incidenten verergeren (retry storm). Gebruik:
- exponential backoff
- jitter
- max retries
- circuit breaker
5.3 Idempotency voor write endpoints
Voor POST/charge/order: gebruik idempotency keys. Dit voorkomt dubbele acties bij retries.
6) Health checks: liveness, readiness, startup
6.1 Liveness vs readiness
- Liveness: “proces leeft nog?” → herstart als vastgelopen.
- Readiness: “kan verkeer aan?” → pas opnemen in load balancer als ready.
- Startup: “heeft tijd nodig om te booten?” → voorkom early kills.
Voor Docker kun je HEALTHCHECK gebruiken en inspecteren:
docker inspect --format='{{json .State.Health}}' <container_id> | jq
Run met healthcheck en kijk status:
docker run -d --name mysvc myservice:local
docker ps
docker inspect mysvc --format '{{.State.Health.Status}}'
docker logs mysvc --tail 50
docker rm -f mysvc
Best practice: readiness endpoint moet afhankelijkheden checken (DB connectiviteit) maar niet te zwaar. Liveness vaak alleen “event loop leeft”.
6.2 Graceful shutdown en SIGTERM
Orchestrators sturen SIGTERM en wachten een grace period. Je app moet:
- SIGTERM afvangen
- stoppen met nieuwe requests
- lopende requests afronden
- resources sluiten
Test met Docker:
docker run -d --name mysvc -p 8080:8080 myservice:local
docker kill --signal=SIGTERM mysvc
docker logs mysvc --tail 100
Controleer dat je app netjes afsluit en exit code 0 geeft.
7) Logging: structuur, correlatie en geen lokale files
7.1 Log naar stdout/stderr
In containers is “log to file” meestal fout (tenzij sidecar/agent). Controleer:
docker run --rm myservice:local 2>&1 | head -n 50
7.2 Structured logging (JSON) + correlation IDs
Gebruik JSON logs met velden:
timestamp,level,service,env,trace_id,span_id,request_id,user_id(indien toegestaan),msg.
Test dat logs parsebaar zijn:
docker run --rm myservice:local 2>&1 | head -n 5 | jq .
7.3 Geen secrets in logs
Zoek op patronen:
docker run --rm myservice:local 2>&1 | grep -iE 'password|secret|token' | head
Beter: implementeer log redaction.
8) Metrics & tracing: SLO’s en diagnose
8.1 Golden signals
Meet minimaal:
- latency (p95/p99)
- traffic (RPS)
- errors (5xx, exceptions)
- saturation (CPU/mem/pool usage)
Expose metrics endpoint (bijv. /metrics voor Prometheus). Test:
docker run -d --name mysvc -p 8080:8080 myservice:local
curl -fsS http://localhost:8080/metrics | head -n 30
docker rm -f mysvc
8.2 Distributed tracing (OpenTelemetry)
Zorg dat je trace context doorgeeft via headers (W3C Trace Context). In productie kun je sampling instellen.
Verifieer dat je service headers accepteert en doorstuurt:
curl -v -H 'traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' \
http://localhost:8080/health || true
9) Resource management: CPU, memory, ulimits, GC
9.1 Stel limieten in
Zonder limieten kan één service de node uitputten. In Docker:
docker run --rm --memory=512m --cpus=1.0 myservice:local
Controleer runtime usage:
docker stats --no-stream
Waarom: voorspelbaarheid en eerlijke scheduling. In Kubernetes zijn requests/limits cruciaal voor autoscaling en stabiliteit.
9.2 OOM gedrag en memory leaks
Test onder load (voorbeeld met hey):
hey -z 30s -c 50 http://localhost:8080/
Kijk naar memory groei:
docker stats mysvc
Als memory blijft stijgen: leak of caching zonder bounds.
9.3 Ulimits en file descriptors
High-throughput services raken snel door file descriptors heen.
Check huidige limieten:
docker run --rm myservice:local sh -lc 'ulimit -n; ulimit -a'
Run met hogere nofile (indien nodig):
docker run --rm --ulimit nofile=65535:65535 myservice:local
10) Data & state: databases, migrations, backward compatibility
10.1 Schema migrations: gecontroleerd en herhaalbaar
Migrations moeten:
- idempotent zijn
- backward compatible (expand/contract pattern)
- apart uitvoerbaar (job/step), niet “automagisch” bij elke pod start (tenzij heel bewust)
Voorbeeld: migration container run (conceptueel):
docker run --rm \
-e DATABASE_URL="$DATABASE_URL" \
myservice:local ./migrate up
10.2 Connection pooling en limits
Zorg dat je pool niet te groot is per replica. Anders overspoel je DB.
Test connectiviteit:
docker run --rm -e DATABASE_URL="$DATABASE_URL" myservice:local ./healthcheck-db
11) CI/CD: build, test, scan, sign, promote
11.1 Automatische tests in pipeline
Minimaal:
- unit tests
- integration tests (met ephemeral DB)
- contract tests (bij microservices)
- smoke tests na deploy
Run tests in container:
docker build -t myservice:test --target test .
docker run --rm myservice:test
11.2 Vulnerability scanning (image + dependencies)
Gebruik een scanner zoals Trivy (voorbeeld):
trivy image myservice:ci
trivy fs --scanners vuln,secret .
Interpretatie: focus op exploiteerbare CVE’s in runtime layers. Fix door base image updates en dependency bumps.
11.3 SBOM genereren
SBOM (Software Bill of Materials) helpt bij compliance en incident response.
Met Syft:
syft myservice:ci -o spdx-json > sbom.spdx.json
11.4 Image signing en provenance
Supply-chain hardening: sign images (cosign). Voorbeeld:
cosign sign --key cosign.key myregistry.example.com/myservice@sha256:<digest>
cosign verify --key cosign.pub myregistry.example.com/myservice@sha256:<digest>
Gebruik digests in deploy manifests zodat je exact weet wat draait.
11.5 Promote via registries, niet rebuilden
Build één keer, promote dezelfde digest van staging naar prod. Dit voorkomt “works in staging, fails in prod” door rebuild drift.
12) Deployment strategie: rolling, blue/green, canary
12.1 Rolling updates met readiness gates
Zonder readiness kan verkeer naar half-opgestarte pods. Zorg dat:
- readiness pas “true” wordt als dependencies ok zijn
- terminationGracePeriod voldoende is
12.2 Canary releases
Stuur een klein percentage verkeer naar nieuwe versie. Meet golden signals en rollback bij regressie.
Praktisch: kanary via ingress/service mesh of load balancer weights.
12.3 Feature flags
Gebruik feature flags voor risicovolle veranderingen. Zo kun je zonder redeploy uitschakelen.
13) Incident response: debugbaarheid en forensics
13.1 Debug tooling: minimalisme vs noodzaak
Minimal images missen tools (curl, dig). In productie wil je vaak:
- óf een aparte “debug image”
- óf ephemeral debug containers (Kubernetes)
- óf sidecar tooling
Voor Docker kun je in dezelfde network namespace debuggen met een toolbox container:
docker run -d --name mysvc -p 8080:8080 myservice:local
docker run --rm --network container:mysvc nicolaka/netshoot curl -v http://localhost:8080/health
docker rm -f mysvc
13.2 Core dumps en crash diagnostics
Voor native apps: configureer crash dumps veilig (let op privacy). Zorg dat je crash info niet in logs lekt.
13.3 Runbooks en on-call
Minimaal:
- “Wat te checken bij 5xx spikes”
- “Hoe rollbacken”
- “Hoe DB connectie issues te isoleren”
- “Hoe rate limiting aan te zetten”
14) Performance: cold start, caching, load tests
14.1 Cold start meten
Meet starttijd tot readiness:
start=$(date +%s)
cid=$(docker run -d -p 8080:8080 myservice:local)
until curl -fsS http://localhost:8080/ready >/dev/null; do sleep 0.2; done
end=$(date +%s)
echo "Ready in $((end-start))s"
docker rm -f "$cid"
14.2 Load en stress tests
Gebruik hey, wrk of k6. Voorbeeld:
hey -n 20000 -c 200 http://localhost:8080/
Kijk naar p95/p99 en error rates. Combineer met docker stats voor saturatie.
14.3 Caching met bounds
Caches moeten:
- TTL hebben
- max size hebben
- eviction policy hebben
Anders krijg je memory pressure en OOM kills.
15) Betrouwbaarheid: retries, rate limiting, bulkheads
15.1 Rate limiting
Bescherm jezelf en downstream dependencies. Implementeer:
- per-client limits
- global limits
- burst control
Test met snelle requests:
for i in $(seq 1 200); do curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/; done | sort | uniq -c
Je wilt bij overload gecontroleerde 429’s zien in plaats van 500’s.
15.2 Bulkheads
Scheid thread pools/connection pools per dependency zodat één trage dependency niet alles blokkeert.
16) Backups, restore en disaster recovery
16.1 Backups zijn waardeloos zonder restore tests
Maak een restore drill:
- restore naar staging
- run smoke tests
- meet RTO/RPO
Voorbeeld (Postgres dump/restore conceptueel):
pg_dump "$DATABASE_URL" -Fc -f backup.dump
createdb restore_test
pg_restore -d restore_test backup.dump
16.2 Multi-region / failover
Als je business dit vereist: ontwerp voor failover. Denk aan:
- stateless services
- replicated data stores
- DNS/load balancer failover
- consistente secrets/config
17) Compliance en privacy
17.1 Data classificatie
Weet welke data je verwerkt (PII/PCI/PHI). Log geen PII. Masker identifiers waar nodig.
17.2 Audit logging
Voor security-relevante acties (admin changes, permission updates) aparte audit logs met:
- wie (subject)
- wat (action)
- wanneer (timestamp)
- resultaat (success/failure)
- correlatie (request_id/trace_id)
18) Concrete checklist (samenvatting)
Gebruik dit als “go/no-go” voor productie.
Image & build
- Multi-stage build, klein runtime image
- Base image gepind op digest
- Reproduceerbare builds (lockfiles, pinned deps)
- SBOM gegenereerd en opgeslagen
- CVE scan uitgevoerd, beleid voor critical/high
- Image gesigned (cosign) en digest-based deploy
Security
- Niet-root user
- Read-only filesystem waar mogelijk + tmpfs
- Capabilities geminimaliseerd
- no-new-privileges / seccomp / AppArmor beleid
- Secrets via secret store, nooit in image/logs
- Ingress/egress policies (platformafhankelijk)
Config & lifecycle
- Config via env/secret, gevalideerd bij startup
- Graceful shutdown (SIGTERM) en drain
- Health endpoints: liveness/readiness/startup
- Timeouts overal (HTTP, DB, queues)
Observability
- Logs naar stdout/stderr, structured JSON
- Correlation IDs + trace context
- Metrics endpoint + dashboards (golden signals)
- Tracing (OpenTelemetry) met sampling
- Alerts op SLO’s (latency, errors, saturation)
Performance & reliability
- CPU/memory limits en requests (of Docker equivalents)
- Ulimits correct (nofile)
- Load tests uitgevoerd, regressies bekend
- Rate limiting, retries met backoff + circuit breaker
- Idempotency voor kritieke writes
Data & deploy
- Migrations gecontroleerd en backward compatible
- Rollback plan getest
- Canary/blue-green strategie beschikbaar
- Backups + restore drill uitgevoerd
19) Snelle “pre-flight” commando’s (praktisch)
Een compacte set checks die je vaak direct kunt draaien:
# 1) Image info
docker image ls myservice:ci
docker image inspect myservice:ci --format 'ID={{.Id}} Size={{.Size}} Created={{.Created}}'
# 2) Run as non-root?
docker run --rm myservice:ci id
# 3) Health endpoints (voorbeeld)
cid=$(docker run -d -p 8080:8080 --name mysvc myservice:ci)
curl -fsS http://localhost:8080/health
curl -fsS http://localhost:8080/ready
docker logs mysvc --tail 50
# 4) Graceful shutdown
docker kill --signal=SIGTERM mysvc
sleep 2
docker logs mysvc --tail 100
docker rm -f mysvc >/dev/null 2>&1 || true
# 5) CVE scan (Trivy)
trivy image myservice:ci
Pas endpoints en poorten aan naar jouw service.
20) Afronding: hoe je deze checklist gebruikt
- Gebruik dit document als Definition of Done voor elke microservice.
- Automatiseer zoveel mogelijk in CI: build, test, scan, SBOM, sign, policy checks.
- Maak observability en security niet optioneel: het zijn productie-eisen, geen “nice to have”.
- Herhaal periodiek: base images en dependencies veranderen, en dus ook je risicoprofiel.
Als je wilt, kan ik deze checklist omzetten naar een concrete CI pipeline-sectie (bijv. GitHub Actions/GitLab CI) met exact dezelfde stappen (build → test → trivy → syft → cosign → push) en een bijbehorend release-proces op basis van image digests.