← Back to DevBytes

Docker Security Best Practices: Best Practices and Common Pitfalls

Introduction to Docker Security

Docker security is the discipline of protecting containerized workloads, the Docker daemon, the host operating system, and the supply chain (images, registries) from malicious or accidental threats. It spans image construction, runtime configuration, networking, secrets management, and the entire CI/CD pipeline. Because containers share the host kernel, a single misconfiguration—such as running a container with excessive privileges—can allow an attacker to escape the container and compromise the entire host.

This tutorial presents essential best practices and highlights common pitfalls that developers, DevOps engineers, and platform teams must understand to build and deploy Docker containers securely.

Why Docker Security Matters

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Modern applications rely heavily on containers for isolation, portability, and efficiency. However, the default Docker settings are optimised for ease of use, not for security. Without deliberate hardening, you expose yourself to risks such as:

Adopting security best practices from the start is far easier than retrofitting them after an incident.

Best Practices for Docker Security

1. Choose Minimal and Trusted Base Images

Start with official, verified images from trusted registries, and prefer minimal flavours (Alpine, slim, distroless). A smaller image contains fewer packages and thus fewer potential vulnerabilities. Avoid pulling generic latest tags for production—always pin a specific version or digest.

# Bad: large, unpinned, unnecessary tools
FROM node:latest

# Good: pinned, slim, and verified
FROM node:18.17-alpine@sha256:a1b2c3d4...

Distroless images (e.g. gcr.io/distroless/nodejs) contain only your application and essential runtime libraries—no shell, package manager, or utilities—reducing the attack surface drastically.

2. Run as a Non-Root User

By default, Docker containers run as the root user (UID 0). If an attacker compromises a process inside the container, they gain root access within that container, which can be leveraged to affect the host (e.g. via misconfigured volumes or capabilities). Create a dedicated user inside the image and use the USER directive.

FROM alpine:3.18
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
COPY --chown=appuser:appgroup ./app /app
CMD ["/app"]

If the application needs to bind to a port below 1024, add only the NET_BIND_SERVICE capability instead of running as root.

3. Use Multi-Stage Builds to Reduce Attack Surface

Multi-stage builds separate compile-time dependencies from the runtime image. The final image contains only the compiled binary and minimal runtime requirements, leaving behind compilers, headers, and build tools that could be exploited.

# Stage 1: build
FROM golang:1.21-alpine AS builder
WORKDIR /src
COPY . .
RUN go build -o /app/server .

# Stage 2: minimal runtime
FROM alpine:3.18
COPY --from=builder /app/server /usr/local/bin/server
RUN adduser -S app && chown app /usr/local/bin/server
USER app
CMD ["server"]

For compiled languages you can even use scratch (an empty image) as the final base, but be aware that debugging tools like a shell won't be available.

4. Scan Images for Vulnerabilities

Integrate vulnerability scanning into your CI pipeline and scan local images before deployment. Tools like Trivy, Docker Scout, Snyk, and Grype analyse image layers against known CVE databases.

# Scan an image with Trivy
trivy image my-app:1.2.3

# Fail the build on critical vulnerabilities
trivy image --severity CRITICAL --exit-code 1 my-app:1.2.3

Regularly update base images and rebuild to pick up fixes for newly discovered CVEs.

5. Limit Container Capabilities and Disable Privilege Escalation

Docker grants a default set of capabilities that can be abused. Drop all capabilities and add only those strictly required. Also, set no-new-privileges to prevent processes from gaining additional privileges via setuid binaries.

docker run --rm -it \
  --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \
  --security-opt=no-new-privileges \
  my-app:1.2.3

Avoid --privileged entirely; it disables all security features of the container runtime and gives the container almost the same access as the host root.

6. Use a Read-Only Root Filesystem

A container that can write to its root filesystem opens a vector for attackers to drop malware, modify binaries, or tamper with configuration. Mount the root filesystem as read-only and use tmpfs for paths that genuinely need writes (e.g. temp directories).

docker run --read-only --tmpfs /tmp --tmpfs /var/run \
  my-app:1.2.3

Ensure the application writes logs and transient data to the designated tmpfs or to mounted volumes, not to the root partition.

7. Secure the Docker Daemon and Its API

The Docker daemon runs with root-equivalent privileges. If an attacker reaches the API socket (usually /var/run/docker.sock), they can create, delete, or escape containers. Secure it with TLS and restrict access.

# /etc/docker/daemon.json
{
  "tls": true,
  "tlsverify": true,
  "tlscacert": "/etc/docker/ca.pem",
  "tlscert": "/etc/docker/server-cert.pem",
  "tlskey": "/etc/docker/server-key.pem",
  "hosts": ["tcp://0.0.0.0:2376", "unix:///var/run/docker.sock"]
}

Enable user namespace remapping (userns-remap) so that the container’s root is mapped to a non-privileged host user, greatly reducing the impact of a container breakout.

8. Manage Secrets Properly

Never embed secrets (API keys, passwords, certificates) in a Dockerfile or in environment variables that end up in the image layer history. Use Docker secrets (Swarm mode), external secret managers (HashiCorp Vault, AWS Secrets Manager), or mount secrets as files into the container at runtime.

# Create a Docker secret (requires Swarm mode)
echo "supersecret" | docker secret create db_password -

# In docker-compose.yml (Swarm)
services:
  app:
    image: my-app:1.2.3
    secrets:
      - db_password
secrets:
  db_password:
    external: true

For non-Swarm setups, mount a read-only secrets file via volume or use an init container to fetch secrets and place them on a tmpfs.

9. Isolate Networks and Avoid Host Mode

Use custom bridge networks to isolate containers that don't need to communicate. Never use --net=host unless absolutely required—it removes network isolation and exposes all host ports directly.

# Create an isolated network
docker network create --internal backend

docker run -d --net backend --name db postgres:15-alpine
docker run -d --net backend --name app my-app:1.2.3

Expose only necessary ports and avoid binding to 0.0.0.0 when possible; use 127.0.0.1 or specific interfaces.

10. Keep Docker and Dependencies Updated

An outdated Docker engine, old container runtimes, or stale kernel versions may have known escape vulnerabilities (e.g. CVE-2019-5736). Use the latest stable Docker version, keep the host OS patched, and regularly rebuild images with updated base layers.

11. Implement Health Checks and Centralised Logging

A HEALTHCHECK instruction helps orchestrators detect anomalies (e.g. a stuck process) and trigger recovery. Send logs to an external system so you can detect and investigate suspicious activity.

FROM nginx:1.25-alpine
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD curl -f http://localhost/ || exit 1
COPY app /usr/share/nginx/html

Common Pitfalls and How to Avoid Them

Pitfall 1: Using the latest Tag in Production

latest is a moving target—it can pull a completely different image tomorrow, breaking your app or introducing vulnerabilities.

# Risky
FROM python:latest

# Safe: pin a specific version and digest
FROM python:3.11.5-slim@sha256:deadbeef...

Pitfall 2: Running Containers as Root Without a Reason

Many tutorials show commands like docker run -it ubuntu without switching users. This leads to containers with root-equivalent privileges.

# Default root run – dangerous
docker run -d --name web nginx:latest

# Inspect user: shows root
docker top web

Always use the USER directive or --user flag.

Pitfall 3: Mounting the Docker Socket Inside a Container

Exposing /var/run/docker.sock to a container is equivalent to giving it root access on the host. Attackers can start a privileged container, mount the host filesystem, and escape.

# Dangerous: mounting the socket
docker run -v /var/run/docker.sock:/var/run/docker.sock my-ci-tool

Alternatives: use a dedicated CI runner with restricted API access, or tools like Kaniko/ Buildah that do not need the socket.

Pitfall 4: Hardcoding Secrets in Dockerfiles or Environment Variables

Secrets in a ENV instruction are baked into the image layers and visible in docker history.

# Never do this
ENV DB_PASSWORD="MySecret123"
COPY .env /app/.env

Use dedicated secret management as described in the best practices.

Pitfall 5: Using --privileged When Specific Devices or Capabilities Suffice

Privileged mode disables all container security, including seccomp and AppArmor. It is rarely needed—most use-cases can be satisfied by adding a specific device or capability.

# Overkill and insecure
docker run --privileged ...

# Instead, add only the required device
docker run --device /dev/fuse --cap-add SYS_ADMIN ...

Pitfall 6: No Resource Limits

A container without memory or CPU limits can exhaust host resources, causing denial of service for neighbouring containers.

docker run -d --name hungry-app --memory 256m --cpus 1.0 my-app:1.2.3

Always set --memory, --cpus, and --pids-limit in production.

Pitfall 7: Mounting Sensitive Host Directories Unnecessarily

Mounting host paths like /etc, /root, or /proc inside a container can expose sensitive configuration or allow privilege escalation.

# Extremely dangerous: gives access to host's shadow file
docker run -v /etc/shadow:/host-data/shadow:ro ...

Limit mounts to the minimum required, use read-only mode, and never mount system-critical directories.

Pitfall 8: Skipping Image Scans

An unscanned image may contain known vulnerabilities (e.g. old OpenSSL versions). Integrate scanning into your CI and fail builds on high-severity findings.

Conclusion

Docker security is a layered practice that spans image selection, build configuration, runtime hardening, network isolation, secrets management, and continuous monitoring. By adhering to the best practices outlined here—minimal images, non-root users, limited capabilities, read-only filesystems, proper secret handling, and regular vulnerability scanning—you drastically reduce the attack surface of your containerised applications.

Equally important is awareness of the common pitfalls: never expose the Docker socket to untrusted containers, avoid privileged mode, pin image versions, and always enforce resource limits. Security is not a one-time audit; it must be embedded into the development workflow, from the Dockerfile to the production orchestrator. Start with a few actionable improvements, automate the checks, and build a culture where containers are secure by default.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles