Skip to content
Innovialabs

Docker Container Security: Best Practices for Production

Practice4 min

In short

A secure container runs as a non-root user from a minimal, scanned image, holds no secrets in its layers, has dropped capabilities and resource limits, and nobody outside the host can reach the Docker socket.

What matters most

A container is not a virtual machine: it shares the kernel with the host. A compromised container with excessive rights can become a compromised server. The core rules for production:

  • Run processes as a non-root user.
  • Build from minimal base images and pin them.
  • Scan images for CVEs in CI before deployment.
  • Never bake secrets into image layers.
  • Drop capabilities, use a read-only filesystem and set resource limits.
  • Protect the Docker socket and the registry.

1. Run as non-root

By default, a process in a container runs as root. If an attacker escapes the application, root inside the container makes the next step much easier.

FROM node:20-alpine
WORKDIR /app
COPY --chown=node:node . .
RUN npm ci --omit=dev
USER node
CMD ["node", "server.js"]
  • Create a dedicated user or use the one the base image provides.
  • Listen on ports above 1024 so root is not needed.
  • Consider rootless Docker or user namespace remapping on the host.

2. Minimal base images

Every extra package is extra attack surface and extra CVEs.

OptionWhen to use
alpine / -slim variantsGeneral services, small size
Distroless imagesCompiled apps and runtimes, no shell
scratchStatic binaries (Go, Rust)
  • Use multi-stage builds: compilers and dev dependencies stay in the build stage.
  • Pin the base image by tag and, for critical services, by digest (image@sha256:...).
  • Rebuild images regularly so security patches in the base image reach production.

3. Scan images for CVEs

  • Add a scanner such as Trivy, Grype or Docker Scout to the CI pipeline.
  • Fail the build on critical vulnerabilities that have a fix available.
  • Scan not only at build time but also images already running, since new CVEs appear every day.
  • Keep a short, documented exception list for findings that do not apply.
trivy image --severity HIGH,CRITICAL --exit-code 1 myapp:1.4.0

4. Keep secrets out of layers

Anything written in a layer stays in the image history, even if a later RUN rm deletes the file. ENV API_KEY=... and COPY .env are visible to anyone who can pull the image.

  • Add .env, keys and credentials to .dockerignore.
  • For secrets needed during the build, use BuildKit secret mounts:
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
  • Pass runtime secrets through the orchestrator: Docker/Swarm secrets, Kubernetes Secrets, or a secrets manager such as Vault or cloud KMS.
  • If a secret was ever pushed in an image, rotate it: deleting the tag is not enough.

5. Limit capabilities and resources

docker run -d \
  --user 1000:1000 \
  --read-only --tmpfs /tmp \
  --cap-drop ALL --cap-add NET_BIND_SERVICE \
  --security-opt no-new-privileges \
  --memory 512m --cpus 1 --pids-limit 200 \
  myapp:1.4.0
  • --cap-drop ALL, then add back only what is needed.
  • Never use --privileged in production.
  • --read-only root filesystem with explicit writable volumes or tmpfs.
  • Memory, CPU and PID limits stop one container from taking down the host.
  • Keep the default seccomp and AppArmor/SELinux profiles enabled.
  • Do not publish database or internal ports to 0.0.0.0; use internal networks.

6. Protect the Docker socket and registry

Access to /var/run/docker.sock equals root on the host. Whoever can talk to it can start a privileged container and mount the host filesystem.

  • Do not mount the socket into application containers. If a tool truly needs it, use a restricted socket proxy.
  • Never expose the Docker API over TCP without TLS client authentication.
  • Limit membership in the docker group on the host.

For the registry:

  • Use a private registry with authentication and per-project access.
  • Give CI a push token and servers a pull-only token.
  • Sign images (for example with cosign) and verify signatures before deployment.
  • Enable automatic scanning in the registry if it is available.

Common mistakes

  • latest tag in production: you never know what is actually running.
  • Secrets in ENV or copied .env files.
  • Socket mounted "just for monitoring".
  • No resource limits, so one memory leak kills every service on the host.

FAQ

Not always. It is small, but uses musl instead of glibc, which can cause compatibility issues for some languages and native libraries. Slim or distroless images are often a better fit.

We can help with this

If you'd rather get the result than figure it all out yourself — this is what we do every day.

Discuss a project

Read next

Have an idea or a task? Tell us about it

We'll reply within one business day and propose a solution. The consultation is free.