Docker Compose Change Review: Find Configuration Regressions Before Deployment
Review a change between two Docker Compose files the way you'd review code: diff it, run each version through a static auditor, and catch privileged mode, mounted Docker sockets, hardcoded secrets, published database ports, and floating tags before they ship.
- #docker
- #docker-compose
- #security-hardening
- #change-review
- #ci-cd
Most Compose problems don’t arrive in a brand-new file. They arrive in a change to a file that already works — a bumped image, a “temporary” port, a socket mount added to make one thing work. The service still starts, the PR still merges, and the regression ships because nobody reviewed the diff the way they’d review application code.
This guide walks a small, realistic change through a repeatable review: read the diff, run each version through a static auditor, interpret the real findings, and separate what a configuration review can prove from what it can’t. Everything here uses synthetic fixtures you can download and run yourself — no production context required.
Quick answer: Treat a Compose change like a code change.
diffthe old and new files, run both through the Docker Production Readiness Auditor, and compare scores. A dropping score with new critical/high findings is your regression list — most often alatesttag, a hardcoded secret, a published datastore port,privileged: true, or a mounted Docker socket. Static review catches configuration regressions; it does not certify runtime security or correctness.
The sample app and the change
The fixtures model a small app with three services — api, db (Postgres), and web (nginx). Download them and follow along:
baseline.compose.yaml— what’s deployed todayproposed.compose.yaml— the proposed change (deliberately regressed — do not deploy)reviewed.compose.yaml— the change after review.env.example,release-review-worksheet.md, and a README
Prerequisites: Docker Engine and the Compose plugin. This walkthrough was tested with Docker 29.6.2 and Docker Compose v5.3.1 (verified 2026-09-16). The auditor is browser-based and needs nothing installed.
Step 1 — Read the diff, not the summary
The single highest-value habit: review the actual diff, because that’s where the regression lives.
diff -u baseline.compose.yaml proposed.compose.yaml
# or, in a repo:
git diff -- '*compose*.y*ml'
Reading our proposed change top to bottom, six things jump out:
apiimage changed fromghcr.io/example/shopdemo-api:1.4.2to:latest.apigainedprivileged: trueandcap_add: [SYS_ADMIN].apigained a volume:/var/run/docker.sock:/var/run/docker.sock.api’sDATABASE_URLbecame a literalpostgres://postgres:S3cr3t-Passw0rd@db:5432/shop, anddb’sPOSTGRES_PASSWORDbecame the literalpostgres.dbgainedports: ["5432:5432"].- The
backendnetwork lostinternal: true, andapi’sdepends_ondropped itscondition: service_healthy.
Each of these “makes it work” in some narrow sense. Each is also a regression. The point of the review is to make that judgment explicit and consistent, not to rely on whoever happens to read the PR.
Step 2 — Validate syntax first (this does not start anything)
Before reasoning about content, confirm the file is even valid. docker compose config renders and validates the merged configuration without creating containers:
docker compose -f proposed.compose.yaml config -q
All three fixtures pass this check (you’ll see harmless warnings that DATABASE_URL/REDIS_URL aren’t set, because they come from .env). A clean config proves the YAML and schema are valid — nothing about whether the change is safe. That’s the next step.
Step 3 — Audit both versions and compare
Static configuration review is exactly the kind of thing worth automating, because humans skim. Run both files through the free Docker Production Readiness Auditor and compare. The point isn’t the absolute number; it’s the delta the change introduces.
Here’s what the auditor (engine v1.0.0) reports for these exact fixtures, verified 2026-09-16:
| File | Score | Band | Critical | High |
|---|---|---|---|---|
baseline.compose.yaml | 94 | Production Ready | 0 | 0 |
proposed.compose.yaml | 74 | Needs Improvement | 2 | 5 |
reviewed.compose.yaml | 94 | Production Ready | 0 | 0 |
A 20-point drop, from 0 to 2 critical and 0 to 5 high findings. That delta is the review. You don’t argue about it in the PR thread; you look at what the change added.
Step 4 — Interpret the real findings
The auditor’s new critical and high findings on proposed.compose.yaml map one-to-one to the diff:
privileged: true— DPA-SEC-101 (critical),services.api.privileged. Privileged mode turns off most of the isolation that makes a container a container: the process gets nearly all capabilities and broad device access. It’s almost never the right answer, and it should never appear in a diff without a written justification.- Docker socket mounted — DPA-SEC-102 (critical),
services.api.volumes[0]. Mounting/var/run/docker.sockgives the container control of the Docker daemon, which is root-equivalent on the host. A compromise of that one service becomes a compromise of the box. SYS_ADMINcapability — DPA-SEC-106 (high),services.api.cap_add[0]. A broad capability added on top of everything else.cap_addentries deserve the same scrutiny assudolines.- Hardcoded secrets ×2 — DPA-SEC-108 (high),
services.api.environment.DATABASE_URLandservices.db.environment.POSTGRES_PASSWORD. A credential in a committed file is a credential in your git history forever. This is the most common real-world Compose regression, and the easiest to fix. - Database port published — DPA-SEC-109 (high),
services.db.ports[0]."5432:5432"binds Postgres to0.0.0.0on the host. On a cloud VM without a tight firewall, that’s an internet-reachable database. latesttag — DPA-SUPPLY-101 (high),services.api.image.:latestmakes the deploy non-reproducible: the nextpullcan bring a different build, and a rollback can’t reliably get you back. Pin a real tag (ideally a digest).
The change also trips a few medium/low findings — api and db lose their healthchecks (DPA-REL-102), and api’s depends_on drops to the start-only form (DPA-REL-103). Those won’t page you the way a public database will, but they’re real reliability regressions and belong in the review notes.
Step 5 — Know what the review can’t prove
Honesty about limits is what separates a useful review from a rubber stamp.
- A static review is not a runtime security test. It reads configuration. It doesn’t scan the image for CVEs, exercise the app, or watch what the container does once it’s running. A perfect score is not a certificate of security.
- Some changes are only partially visible. In our diff, the
backendnetwork droppedinternal: true. The auditor flags the published database port (DPA-SEC-109), but “this network is now externally routable” is best treated as a manual review item — confirm network scope by hand, because the tool reasons per-service, not about your whole topology. - The baseline isn’t a perfect 100 either. Even
baseline.compose.yamlcarries standing low/medium findings — nono-new-privilegesondb/web, unbounded container logging, missing resource limits on some services. Those are deliberate trade-offs or known follow-ups, not regressions from this change. The review is about the delta, so don’t let standing noise distract from what the change introduced.
Step 6 — Fix, then re-audit
Work the release worksheet against the diff and correct each regression. The reviewed.compose.yaml shows the target: pinned tag (1.5.0), secret back in .env, privileged/SYS_ADMIN/socket removed, least-privilege restored (cap_drop: [ALL], no-new-privileges, non-root user, read_only), the DB port unpublished, health gating restored, and backend internal: true again.
Re-audit the reviewed file and the score returns to 94 / Production Ready with zero critical or high findings. That’s the loop: diff → audit → interpret → fix → re-audit, until the change adds no new high-severity findings.
Read the categories, not just the number
A single score is a headline; the category breakdown is where the review lives. The auditor groups findings into security, reliability, performance, observability, and supply-chain, and each contributes to the total. When you compare baseline to proposed, look at which category moved:
- Security is what collapses on our proposed change — privileged mode, the socket mount,
SYS_ADMIN, secrets, and the published port are all security findings, and they’re what turn a 94 into a 74. Security regressions are the ones worth blocking a merge over. - Reliability slips quietly: removing the
apianddbhealthchecks (DPA-REL-102) and droppingdepends_onto the start-only form (DPA-REL-103) won’t show up in a quick read of the diff, but they change how the stack behaves when a dependency is slow to come up. - Supply-chain catches the
latesttag (DPA-SUPPLY-101) — the finding most likely to cause a “worked yesterday, broken today” incident with no code change at all.
The practical habit: don’t argue about the absolute score. Ask “did this change add a security or reliability finding that wasn’t there before?” If yes, that finding is your review comment.
A note on what the score is not: two of our services carry standing observability and performance findings (no log-size cap, no resource limits on web/db) in every version, including the baseline. They’re real, but they’re not what this change introduced. Scoping the review to the delta keeps you from relitigating known trade-offs on every PR.
Wire it into review, not just your laptop
The value compounds when the check runs every time, not just when someone remembers:
- Keep the worksheet as a PR checklist so
privileged, socket mounts, secrets, and published datastore ports get an explicit yes/no. - Add a syntax gate to CI:
docker compose -f compose.yaml config -q. - Run the auditor on the changed file as part of review and paste the before/after scores into the PR.
If you also want to harden the rest of the stack — non-root users, minimal capabilities, health and resource limits across every service — the Docker Compose Core Guide covers the patterns, and the Docker Skills Academy has a hands-on hardening lab that walks the same review on a larger app with a graded solution.
Keep your next release review in Pro
The Auditor is free to run on any file, any time. If you review releases regularly, DevOps AI ToolKit Pro keeps your review history organized across changes — saved, exportable audit reports you can attach to a PR or an incident — alongside the Incident Assistant and Infrastructure Code Review. Practice the fixes here first, then keep your deployment reviews in Pro. Your free per-audit result never goes away; Pro just remembers it for you.
Fixtures and auditor findings in this article are reproducible: download the files, run docker compose config, and run both versions through the auditor. Scores were generated by the site’s own auditor engine (v1.0.0) on 2026-09-16 and will match what you see. The insecure fixture is for review practice only — never deploy it.
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.