In most teams, Docker security is treated at the level of “we ran an image scan, done.” In production, however, the risk surface is much broader: if image supply chain, runtime configuration, and the host/daemon layer aren’t considered together, one day you’ll wake up to a “container escaped to the host” incident.
This guide treats Docker not as “a single tool” but as an operating model. My goal isn’t academic: I want to hand you a checklist that’s applicable in the field, measurable, and wireable into CI/CD.
1) Threat model: “What are we preventing?”
The first step is not a “best practices” list; it’s your own threat model. The scenarios I encounter most often in Docker security:
- Supply chain: Poisoned layers in the base image, dependency typosquatting, tokens leaked through CI.
- Secret leakage: ENV, build args, layer history, logs, crash dumps.
- Privilege escalation:
--privileged,CAP_SYS_ADMIN,docker.sockmount, kernel escape. - Lateral movement: Pivoting to other containers on the same host, internal network discovery, metadata services.
- Operational risk: Wrong tag/pin, no rollback, drift, “it worked on my machine.”
Ask yourself these 4 questions:
| Question | Goal | Typical control |
|---|---|---|
| If code inside the container is malicious, can it escape to the host? | Reduce escape risk | Rootless, seccomp, caps drop |
| If a malicious dependency entered the image, can it reach prod? | Close the supply chain | SBOM, scan, sign, policy gate |
| Where can secrets leak? | Reduce “accident” risk | Build secrets, runtime injection, audit |
| Can we triage quickly during an incident? | Limit blast radius | Log/metric, immutable image, runbook |
2) Image layer: Build-time security and hygiene
Many production incidents start before runtime: packages that go into the image, credentials used during build, the choice of base image…
My minimum standard:
- Minimal base image (small attack surface)
- Digest pinning (prevents tag drift)
- Multi-stage build (the build toolchain doesn’t carry over to runtime)
- Non-root user (don’t run as root by default)
- Deterministic build (lockfile + reproducibility)
Example: a “secure default” Dockerfile
# syntax=docker/dockerfile:1.7
FROM node:22-alpine AS build
WORKDIR /app
# 1) Kilitli bağımlılıklar
COPY package.json package-lock.json ./
RUN npm ci
# 2) Kaynak + build
COPY . .
RUN npm run build
# Runtime image: daha küçük, daha az paket
FROM node:22-alpine AS runtime
WORKDIR /app
# 3) Non-root user
RUN addgroup -S app && adduser -S app -G app
USER app
# 4) Sadece gerekli artefact’lar
COPY --from=build /app/dist ./dist
ENV NODE_ENV=production
CMD ["node", "dist/server/entry.mjs"]
This example isn’t “the best”; it aims for “the safest default.” If you want to go further:
- Distroless instead of
node:alpine(tooling/debug trade-off) - Package additions like
apk addshould be minimal and audited - Version pinning:
FROM node@sha256:...(every build uses the same base)
3) Supply chain: SBOM, scan, signing (a scan isn’t enough)
Saying “I ran a scan” doesn’t mean “no risk.” A scan only gives you a list of “known CVEs.” For production security, I want all 3 pieces together:
- SBOM (Software Bill of Materials): “What’s inside?”
- Vulnerability scan: “Are there known holes?”
- Signature/attestation: “Who built this image, is it the same one?”
In practice, the “minimum” line:
# SBOM + scan örneği (tool seçimi size ait)
trivy image --scanners vuln,secret --format table myapp@sha256:...
# İmzalama örneği (cosign)
cosign sign --key cosign.key myapp@sha256:...
cosign verify --key cosign.pub myapp@sha256:...
Policy gate logic: in CI, tie the questions “are there critical CVEs?” + “is it signed?” + “was an SBOM produced?” to the merge/deploy gate.
4) Secrets: not via build args/ENV, but via the right channel
Secret leakage is usually not “malicious intent” but bad habits:
ARG NPM_TOKEN=...(stays in image history)ENV DB_PASSWORD=...(visible via docker inspect)- Copying a
.envfile into the image
The correct approach: don’t bake the secret into the image, inject it at runtime.
If a build-time secret is required (e.g. private registry)
Use BuildKit’s secret mount (it’s not written into a layer):
# syntax=docker/dockerfile:1.7
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN="$(cat /run/secrets/npm_token)" npm ci
Injecting secrets at runtime
- Docker secrets (Swarm) / orchestrator secret store
- Kubernetes secret + CSI driver / external secret manager
- Short-lived token (OIDC) + service identity
5) Runtime hardening: capabilities, seccomp, read-only FS
The runtime goal: even if the process inside the container “goes bad,” limit its impact.
Safe run flags (Docker run)
docker run --rm \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--pids-limit 256 \
--memory 512m --cpus 1 \
--security-opt no-new-privileges:true \
--cap-drop ALL \
--cap-add NET_BIND_SERVICE \
--user 10001:10001 \
--network myapp-net \
myapp@sha256:...
What we did here:
- read-only rootfs: cuts file write capability (persistence becomes harder)
- tmpfs: provides the writable area you need, but controlled (noexec/nosuid)
- pids/memory/cpu limit: limits the impact of fork bombs / DoS
- no-new-privileges: closes off “gain privileges later” paths like setuid
- cap-drop: minimizes kernel privileges
seccomp / AppArmor / SELinux
Docker’s default seccomp profile isn’t bad, but for critical workloads you need a tighter profile. Minimum approach:
- Disabling default seccomp (
--security-opt seccomp=unconfined) is forbidden - AppArmor/SELinux on whenever possible
- For prod, derive a “golden profile” (which syscalls do you actually need?)
6) Host and daemon: the most-forgotten layer
No matter how good your containers are, if the host side is weak, the game is over.
What I expect at minimum on the host side:
- Kernel patches kept current, with a reboot discipline
- Docker daemon access restricted (
dockergroup membership under control) - Rootless preferred where possible
- Logging/audit on and centralized
- Image cache and registry access controlled
Sample hardening settings for the Docker daemon
{
"live-restore": true,
"no-new-privileges": true,
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "5" }
}
This file usually lives at /etc/docker/daemon.json. Don’t “copy-paste” every setting; test first.
7) Security gates in CI/CD: “the right to deploy” is a policy decision
Make security a pipeline rule, not a doc:
- On PR: SAST + secret scan
- On build: SBOM + vulnerability scan
- On publish: sign + provenance
- On deploy: policy check (signed? critical CVEs?)
The good thing about this approach: “you don’t have to trust anyone,” because the system already won’t allow it.
8) Quick note on Kubernetes: same principle, different mechanism
What you do in Docker with docker run, in Kubernetes you express via securityContext:
runAsNonRoot: truereadOnlyRootFilesystem: trueallowPrivilegeEscalation: falsecapabilities: drop: ["ALL"]
And at the policy layer:
- Pod Security Standards / PSA
- Admission policy (OPA/Gatekeeper, Kyverno)
- Lateral movement control with NetworkPolicy
9) Incident runbook: suspicious container behavior
If a container is behaving “weird” (unexpected outbound traffic, suspected crypto miner, spike):
- Network isolation: cut egress (in the first 2 minutes)
- Image + digest: which digest is running? Is it the same as in the registry?
- Process list: any unexpected processes? (
ps,ss,lsof) - Filesystem writes: if not read-only, which path was written to?
- Credential exposure: log access to ENV/volume/secrets
- Host signal: kernel audit / runtime alerts (Falco/eBPF)
- Root cause: supply chain, misconfig, or exploit?
Conclusion: Minimum secure profile (copy + adapt)
If you’re saying “I can’t do all of this at once,” sequence it like this:
- Digest pin + multi-stage + non-root
- Pull secrets out of the image (runtime injection)
--cap-drop ALL+no-new-privileges+ read-only rootfs- SBOM + scan + sign + policy gate
- Host patching + rootless standard + audit/log pipeline
Security isn’t a single setting; it’s design + automation + operational rhythm. The best indicator: when a “bad image” arrives one day, does your system refuse to admit it to prod?