Pre-Deploy Checklist

Docker deployment security checklist

Every item in this checklist maps to an OWASP rule, a CIS Docker Benchmark control, or a finding Keelix catches automatically. Work through it once before you ship; automate it in CI so it runs every time.

01

Image hardening

CRIT

Run as a non-root user

Add a USER instruction in your Dockerfile. The container default is root (uid 0). If an attacker escapes the container, they have host root privileges unless you map to an unprivileged uid. OWASP Rule #2.

WARN

Use a minimal base image

Prefer distroless, alpine, or slim variants. Fewer packages means fewer CVEs in the base layer and a smaller attack surface. Verify with `docker scan` or `keelix scan`.

CRIT

No secrets baked into the image

Never COPY a .env file or hard-code credentials in a Dockerfile ENV instruction. Anything in a layer is readable with `docker history`. Use build secrets (BuildKit) or inject at runtime via Docker secrets.

WARN

Pin image tags to a digest

FROM node:20-alpine is mutable — the tag can be silently updated with a different image. FROM node:20-alpine@sha256:<digest> is immutable. Pin digests for production images.

WARN

Scan for CVEs in the image

Run a vulnerability scanner (Trivy, Grype, or keelix scan) against the image before deployment. Block on critical findings. OWASP Rule #9.

02

Runtime configuration

CRIT

Drop all capabilities; re-add only what's required

--cap-drop=ALL removes every Linux capability. Then --cap-add=NET_BIND_SERVICE re-adds only what that container legitimately needs. Most application containers need zero capabilities. OWASP Rule #3.

CRIT

Set --security-opt=no-new-privileges

Prevents setuid/setgid binaries inside the container from escalating privileges. A single flag that blocks a broad class of privilege escalation. OWASP Rule #4.

CRIT

Never use --privileged

--privileged gives the container nearly all Linux capabilities and turns off seccomp and AppArmor profiles. It is equivalent to root on the host. Required only for very specific system containers.

WARN

Mount the filesystem read-only where possible

--read-only with --tmpfs=/tmp (for writable scratch space) prevents an attacker who gains code execution from persisting changes to the container filesystem. OWASP Rule #8.

INFO

Apply seccomp / AppArmor profiles

Docker's default seccomp profile blocks ~44 syscalls. Custom profiles restrict further. AppArmor profiles are the standard on Debian/Ubuntu. OWASP Rule #6.

WARN

Set resource limits (memory, CPU, pids)

Without limits, a single runaway container can exhaust all host resources. Add --memory=512m --cpus=0.5 --pids-limit=200 or equivalent deploy.resources in compose. OWASP Rule #7.

INFO

Run Docker in rootless mode

Rootless mode runs the daemon and containers as the invoking non-root user. This substantially reduces the blast radius of daemon compromise. OWASP Rule #11.

03

Network exposure

CRIT

Bind published ports to 127.0.0.1, not 0.0.0.0

Docker's -p flag bypasses UFW. A port mapped as 8080:80 binds to 0.0.0.0 and is reachable from the internet regardless of your UFW rules. Use 127.0.0.1:8080:80 for internal services. OWASP Rule #5a.

CRIT

Never expose the Docker daemon API on TCP without mTLS

Port 2375 (unencrypted) gives unauthenticated root-equivalent access to the entire host. If you need remote Docker access, use SSH tunnelling (DOCKER_HOST=ssh://user@host) rather than TCP. OWASP Rule #1.

CRIT

Do not mount /var/run/docker.sock into containers

Mounting the Docker socket into a container is equivalent to giving that container full root access to the host. Any container that can speak to the socket can launch new containers, read secrets, and escape. OWASP Rule #1.

WARN

Use custom bridge networks; disable inter-container communication where not needed

Containers on the default bridge network can reach each other by IP. Create purpose-built networks so only containers that need to communicate are on the same network. OWASP Rule #5.

04

Secrets management

CRIT

Use Docker secrets or a vault — not environment variables

Environment variables are visible in docker inspect, in /proc/<pid>/environ, and in many logging pipelines. Use Docker secrets (mounted at /run/secrets/<name>) or an external vault (HashiCorp Vault, AWS Secrets Manager) for production credentials.

CRIT

Check MCP config files for plaintext API keys

Claude Code, Cursor, Windsurf, and other AI coding agents store MCP server configurations at well-known paths. These files routinely contain API keys and tokens in plaintext. Run keelix scan — it reads these paths and flags plaintext secrets.

WARN

Rotate credentials after any exposure event

If a secret was ever in a Docker image layer, an env var, or a public repository, treat it as compromised and rotate it immediately. Audit your git history with tools like git-secrets or truffleHog.

05

AI agents & MCP servers

CRIT

No agent auto-approval on production boxes

Agents configured to approve all MCP tool calls without user confirmation turn a prompt-injection finding into a full blast-radius event. Verify no .mcp.json or agent config enables auto-approve in production.

CRIT

Check for the lethal trifecta

If a deployed agent has access to private data, is exposed to untrusted input, and has a reachable exfiltration channel — all three present — it is a high-severity finding. Keelix checks all three legs deterministically.

WARN

Verify MCP servers against known publishers

Unvetted MCP servers with no verified publisher or recent package history are a supply-chain risk. Review your agent config before each deployment and cross-reference installed servers.

06

Host & supply chain

WARN

Keep the Docker Engine and host kernel patched

Container escapes and privilege escalation CVEs regularly target the kernel and the Docker Engine. Run unattended-upgrades (Ubuntu/Debian) or equivalent. OWASP Rule #0.

INFO

Use signed images and verify provenance

Enable Docker Content Trust (DOCKER_CONTENT_TRUST=1) or use cosign for image signing. Verify SBOMs for critical images. OWASP Rule #13.

INFO

Enable container activity logging at info level

The Docker daemon's default log level is 'info' — do not lower it to 'warning' or 'error'. Retain logs for audit and incident response. OWASP Rule #10.

Automate the whole checklist

GitHub Actions — block on any CRIT finding
- name: Keelix pre-deploy gate
  uses: jakelamon/keelix@v0.1.0
  with:
    threshold: 80
    fail-on: critical

Exit code 0 means the posture score meets the threshold and no blocked findings triggered. Exit code 1 means something in this checklist failed — with the finding, severity, and control ID in the output.

The surface most checklists miss

Frequently asked

What should I check before deploying a Docker container to production?
Before deploying, verify: (1) your image runs as a non-root user; (2) no secrets are baked into the image or passed via environment variables — use Docker secrets or a vault; (3) published ports bind to 127.0.0.1, not 0.0.0.0, unless you explicitly need public access; (4) the Docker daemon socket is not mounted into any container; (5) capabilities are dropped with --cap-drop=ALL and only the required ones are re-added; (6) --security-opt=no-new-privileges is set; and (7) any AI agents or MCP servers on the box have no auto-approval enabled and no plaintext credentials in their config files. Run keelix scan to verify all of the above in a single automated pass.
What is a docker-compose security checklist?
In docker-compose.yml, each service should include: read_only: true on the filesystem, security_opt: [no-new-privileges:true], cap_drop: [ALL] with only necessary capabilities re-added in cap_add, ports bound to 127.0.0.1 for internal services, and resource limits (deploy.resources.limits) to prevent denial-of-service. Secrets should use the top-level secrets: block referencing a mounted file or Docker Swarm secret — not environment variables. The OWASP Docker Security Cheat Sheet and Docker Docs both cover these patterns.
How do I stop Docker from bypassing my firewall (UFW)?
Docker inserts iptables rules directly into the DOCKER chain, which is evaluated before UFW's FORWARD rules. A UFW deny rule will not block a Docker-published port. The correct fix is to bind the host port to 127.0.0.1 in the -p mapping: -p 127.0.0.1:8080:80 instead of -p 8080:80. In docker-compose.yml: ports: ['127.0.0.1:8080:80']. Services that need to be publicly reachable should sit behind a reverse proxy (Nginx, Caddy, Traefik) — the proxy binds to the public interface; internal services bind to localhost only.
How do I check if my Docker setup is secure?
Run keelix scan on the host. It checks over 90 deterministic controls across image config, container runtime, host settings, network exposure, and AI/MCP agents — and produces a 0–100 posture score. For the subset of CIS Docker Benchmark checks, docker-bench-security (the official CIS reference tool) produces a pass/warn/fail log you can also review. Keelix automates both and adds the outside-in exposure check and AI/MCP surface that docker-bench-security does not cover.

Run the whole checklist in under two minutes.

Free, local-first, Apache-2.0. Over 90 deterministic checks including the OWASP rules, CIS Benchmark controls, and the AI/MCP surface. Nothing leaves the box.

operator@host — keelix
# install — free & open source (Apache-2.0)
$ curl -fsSL https://keelix.dev/install.sh | sh
$ keelix scan