July 12, 2026 · Docker HQ Team

Docker Security Best Practices for Production

Docker security best practices in production come down to a few things you can actually control: don't run containers as root, drop the capabilities you don't need, keep your images small and patched, lock down the daemon socket, and watch what's running so you notice when something breaks. Most breaches happen because one of these got skipped, not because of some exotic zero-day. This guide walks through the practices that matter, with the commands to apply them.

Start with the container user

The single most common mistake is running everything as root inside the container. If an attacker breaks out of a root process, they land on the host with far more leverage. Fix it in the Dockerfile.

FROM node:20-slim
RUN useradd --system --uid 10001 --no-create-home appuser
WORKDIR /app
COPY --chown=appuser:appuser . .
RUN npm ci --omit=dev
USER appuser
CMD ["node", "server.js"]

The USER instruction is the line that counts. Put it after your installs, since package managers usually need root. You can double-check what a running container thinks it is:

docker exec my-container id
# uid=10001 gid=0 groups=0

If you can't rebuild the image, you can still force a non-root user at runtime:

docker run --user 10001:10001 nginx:alpine

And block privilege escalation entirely, so a setuid binary inside the container can't bump itself back up to root:

docker run --security-opt no-new-privileges:true myapp

Drop capabilities and go read-only

By default Docker grants a container a chunk of Linux capabilities it almost never uses. The safe pattern is to drop everything, then add back only what the process needs.

docker run \
  --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \
  --read-only \
  --tmpfs /tmp \
  myapp

--cap-drop=ALL strips the lot. NET_BIND_SERVICE is the one you add back if you bind to a port under 1024. --read-only mounts the root filesystem read-only, which stops an attacker from writing a payload to disk, and --tmpfs /tmp gives the app a writable scratch space that vanishes on restart.

Never reach for --privileged unless you genuinely know why. It disables almost every isolation feature Docker gives you. If you think you need it, you probably need a specific capability or device instead.

Keep the attack surface small

A smaller image has fewer packages, which means fewer CVEs to patch and less for an attacker to work with. Prefer -slim or alpine base images, or distroless where the language allows it. A Go binary needs almost nothing around it:

FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./cmd/server

FROM gcr.io/distroless/static-debian12
COPY --from=build /app /app
USER 10001
ENTRYPOINT ["/app"]

Multi-stage builds keep your compiler, source, and build tools out of the shipped image. The final layer has the binary and nothing else, so there's no shell to drop into if something goes wrong.

Pin your base images by digest rather than a floating tag. node:20-slim can change under you; node:20-slim@sha256:... can't. It makes builds reproducible and stops a poisoned upstream tag from silently entering your pipeline.

Scan images before they ship

You can't patch what you don't know about. Scan every image in CI and fail the build on high-severity findings. Trivy is a solid free option:

trivy image --severity HIGH,CRITICAL --exit-code 1 myapp:latest

Docker's own scanner works too:

docker scout cves myapp:latest

Scanning once at build time isn't enough, though. New CVEs get published against images you shipped weeks ago, so rescan what's actually running on a schedule. We go deeper on tooling and how to read the output in Docker Image Vulnerability Scanning: A Practical Guide.

Handle secrets properly

Do not bake secrets into images or pass them as plain ENV variables. They end up in image layers, docker inspect output, and shell history. Anyone who pulls the image can read them.

Use Docker's secret mounts, which land in memory at /run/secrets and never touch the image:

echo "s3cr3t" | docker secret create db_password -

For build-time secrets, BuildKit mounts them without writing to a layer:

docker build --secret id=npmtoken,env=NPM_TOKEN .
RUN --mount=type=secret,id=npmtoken \
    NPM_TOKEN=$(cat /run/secrets/npmtoken) npm ci

There are real tradeoffs between Compose secrets, Swarm secrets, and an external vault. Docker Secrets Management: What Actually Works covers which one to pick for your setup.

Lock down the daemon and the socket

The Docker daemon runs as root, so access to it is effectively root on the host. Two rules follow from that.

First, never mount /var/run/docker.sock into a container you don't fully trust. A container with the socket can start a new privileged container mounting the host root filesystem, which is a trivial escape. If a tool needs Docker access, look at a rootless setup or a socket proxy that filters the API calls it can make.

Second, if you expose the daemon over TCP for remote management, never leave it on plaintext 2375. That's an unauthenticated root shell open to the network. Use TLS client-certificate auth on 2376, or better, tunnel over SSH and leave the API bound to localhost. The full setup is in How to Secure the Docker Remote API.

# SSH tunnel beats an exposed TCP port
docker -H ssh://user@prod-host ps

Set resource limits

A container with no limits can eat all the host's memory or CPU, whether from a bug or a fork bomb. Cap it.

docker run \
  --memory=512m \
  --cpus=1.5 \
  --pids-limit=200 \
  myapp

--pids-limit is the one people forget, and it's the one that stops a fork bomb from taking the host down. Set it.

Watch what's actually running

Hardening the config is half the job. The other half is knowing when something changes at 3am. A container that keeps restarting, a host whose disk is filling with logs, an image that just picked up a critical CVE. You want to hear about these before your users do.

This is where checking on things from your phone actually helps. Docker HQ connects to your hosts over SSH and pushes an alert when a container or host goes down, so you can open a shell, read the logs, and restart a service without getting to a laptop. It also runs image scans and shows host CPU, RAM, and disk per server, which is enough to catch the slow-burn problems before they page you. If you manage more than one host or share access with a team, the team and RBAC plans keep who-can-touch-what under control.

The Docker security best practices checklist

Before a container hits production, run through this:

  • Runs as a non-root USER, with no-new-privileges set.
  • --cap-drop=ALL plus only the capabilities it needs.
  • Read-only root filesystem, writable paths on tmpfs or named volumes.
  • Base image pinned by digest, kept slim, rebuilt on a schedule.
  • Image scanned in CI, build fails on HIGH/CRITICAL.
  • No secrets in ENV or image layers.
  • Memory, CPU, and PID limits set.
  • Daemon socket not mounted into untrusted containers; remote API behind TLS or SSH.

Frequently asked questions

Is running a container as root ever okay?

Rarely, and only during setup steps like installing packages or binding privileged ports at startup. The long-running process should drop to a non-root user. If a container must stay root, isolate it further with a read-only filesystem, dropped capabilities, and no-new-privileges so a breakout has less to work with.

What's the difference between --cap-drop and --privileged?

--privileged gives a container nearly all host capabilities and disables most isolation, so treat it as "root on the host." --cap-drop=ALL does the opposite: it removes every capability, and you add back only the specific ones your process needs. Start from --cap-drop=ALL and never use --privileged unless a device genuinely requires it.

How often should I scan images that are already deployed?

At least weekly, and after any major CVE disclosure. A scan that passed at build time only reflects the vulnerabilities known that day. New CVEs get filed against packages in images you shipped a month ago, so rescanning running images on a schedule is what catches the ones you couldn't have known about.

Should I use rootless Docker in production?

If your workloads don't need host-level privileges, rootless Docker removes a large class of escape risks by running the daemon and containers as an unprivileged user. It has limits around some networking and storage drivers, so test your stack against it first. For many web services it works with no changes.

Where to start

Don't try to apply all of this at once. Pick your most exposed service, add a non-root USER and --cap-drop=ALL, and confirm it still runs. That one change closes the biggest gap. Then move outward: resource limits, image scanning in CI, and locking down the daemon socket. Each step is small, and each one removes a way in.