Container Security: Hardening Docker in Production

Secure your container pipeline from image building to runtime. Covers minimal base images, vulnerability scanning, non-root execution, runtime security policies, secrets injection, and the defense-in-depth approach that prevents container breakouts.

This page describes practical steps to secure Docker containers in production environments, focusing on configurations and practices that prevent container escape, privilege escalation, and data leakage. These techniques apply to any container runtime that supports standard Docker image formats and Linux capabilities.

Run Containers with Minimal Privileges

The default Docker container runs as root inside the container, but this user has full host privileges when mounted volumes are involved. To avoid privilege escalation:

docker run --user=1000:1000 alpine:latest sh

Use non-root UIDs for application processes. The UID 1000 should be mapped to a user in the container's /etc/passwd file. This prevents an attacker who compromises a container process from gaining host-level access.

Avoid Running as Root

Running containers with root privileges (--user=0:0) creates opportunities for privilege escalation attacks. If a container has mounted volumes, processes running as root inside can write to host directories with arbitrary permissions.

Drop Capabilities and Use Read-Only Filesystems

Docker containers should drop unnecessary Linux capabilities and mount filesystems as read-only where possible.

docker run --cap-drop=ALL \
  --read-only \
  --tmpfs=/tmp \
  --tmpfs=/var/tmp \
  alpine:latest sh

The --cap-drop=ALL flag removes all Linux capabilities except those needed for the application. The most common required capability is CAP_SETGID, which allows changing group IDs. Applications requiring network access may need CAP_NET_BIND_SERVICE.

Common Capabilities to Keep

If your container needs network binding:

docker run --cap-drop=ALL \
  --cap-add=CAP_NET_BIND_SERVICE \
  --read-only \
  alpine:latest sh

Configure Seccomp Profiles

Use seccomp profiles to restrict system calls available inside the container.

docker run --security-opt seccomp=/path/to/profile.json alpine:latest sh

The profile JSON should define a whitelist of allowed system calls. A minimal profile might allow only:

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "syscalls": [
    {
      "name": "execve",
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

Use Immutable Base Images

Avoid building containers with mutable base images. Pin image tags to specific digests:

docker run sha256:7d3d19b8c3f4a1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6 alpine:latest sh

Check Image Digests

Verify that images use immutable digests by running:

docker inspect <image_name> | grep -A 5 "Image"

The Image field should show a full SHA256 digest instead of a tag like alpine:latest.

Set Resource Limits

Control CPU and memory usage to prevent resource exhaustion attacks:

docker run --memory=128m --cpus="0.5" alpine:latest sh

Prevent Memory Exhaustion

Without memory limits, containers can consume all host RAM. This leads to OOM killer behavior that may terminate unrelated processes on the host.

Secure Volume Mounts

When mounting volumes, ensure they are not writable by root inside the container:

docker run -v /host/path:/container/path:ro alpine:latest sh

Bind Mounts and UID Mapping

If you must mount a volume as writable, ensure it's owned by the non-root user:

docker run -v /host/path:/container/path:rw \
  --user=1000:1000 \
  alpine:latest sh

Configure Health Checks and Liveness Probes

Implement health checks to detect container failures quickly:

docker run --health-cmd="curl -f http://localhost:8080/health || exit 1" \
  --health-interval=30s \
  --health-timeout=10s \
  alpine:latest sh

Health Check Failure Modes

If a container's health check fails, Docker may restart it without informing operators. This can mask application-level issues and create false sense of stability.

Use Container Runtime Sandboxing

When available, enable sandboxing features in the container runtime:

docker run --security-opt no-new-privileges=true alpine:latest sh

The no-new-privileges flag prevents processes from gaining new privileges even if they have setuid binaries. This blocks privilege escalation attacks.

Limit Network Access

Restrict network access to only necessary services:

docker run --network=none alpine:latest sh

Default Network Configuration

By default, Docker containers can reach the host network through the bridge interface. This creates an attack surface where a compromised container can scan host ports or establish reverse connections.

Implement Container Image Scanning

Scan images for known vulnerabilities before deployment:

docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy image alpine:latest

Vulnerability Scan Output Format

Trivy's output includes severity levels, CVE identifiers, and affected packages. The scan results should be used to either:

  1. Update the base image to a version without critical vulnerabilities
  2. Add additional runtime protections (like capability drops)

Secure Docker Daemon Configuration

Configure the Docker daemon to enforce security policies:

{
  "userland-proxy": false,
  "icc": false,
  "userland-proxy": false,
  "no-new-privileges": true
}

Daemon Configuration Key Explanation

The icc (inter-container communication) setting should be set to false to disable implicit container-to-container networking. This prevents containers from connecting to each other without explicit port exposure.

Implement Secrets Management

Store secrets outside the container image:

docker run --env-file=/path/to/secrets.env alpine:latest sh

Environment File Format

The secrets file should contain:

DATABASE_PASSWORD=secret123
API_KEY=another_secret_key

Never store sensitive data in Docker image layers. Use external secret management systems or Kubernetes secrets when orchestrating containers.

Monitor Container Logs and Audit Events

Configure logging to capture security-relevant events:

docker run --log-driver=json-file \
  --log-opt max-size=10m \
  --log-opt max-file=3 \
  alpine:latest sh

Log Rotation Configuration

The log driver configuration prevents disk space exhaustion from excessive logging. When logs exceed the maximum size, Docker rotates them automatically.

Handle Container Lifecycle Events

Implement proper cleanup procedures for container termination:

docker run --stop-timeout=30 alpine:latest sh

Stop Timeout Behavior

A container's graceful shutdown period should be at least 30 seconds. If a container doesn't terminate within this time, Docker forcefully kills it, which can lead to data corruption or incomplete cleanup.

Use Container Image Signing

Sign container images to ensure integrity:

docker run --pull=always gcr.io/distroless/base:latest

Pull Policy Configuration

The --pull=always flag ensures that the latest image version is always pulled from the registry, reducing the chance of running unsigned or compromised images.

Secure Container Networking with Policies

When using container orchestrators, define network policies:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-all
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress

Network Policy Implementation

This policy blocks all ingress and egress traffic for pods that match the selector. It should be applied to containers that do not need external network access.

Configure Container Runtime Isolation

Use runtime-specific features for additional isolation:

docker run --cgroup-parent=/containers/myapp \
  --memory-swap=256m \
  --oom-score-adjust=-500 \
  alpine:latest sh

Memory Swap Configuration

The --memory-swap flag sets total memory including swap space. This prevents containers from being killed by the OOM killer when memory usage is high but still within limits.

Validate Container Security at Runtime

Test container configurations with runtime checks:

docker run --rm alpine:latest sh -c 'id && cat /proc/self/status'

Status File Inspection

The /proc/self/status file shows the current process's UID, GID, and capabilities. Verify that these match the expected security context after starting the container.

Handle Container Escape Prevention

Prevent containers from accessing host resources through mount propagation:

docker run --mount type=bind,source=/host/path,target=/container/path,bind-propagation=private alpine:latest sh

Mount Propagation Settings

The bind-propagation=private setting ensures that mount events on the host do not propagate into the container and vice versa. This prevents containers from affecting host filesystems through shared mounts.

Secure Container Registry Access

Configure registry authentication to prevent unauthorized image pulls:

docker login registry.example.com

Credential Management

Docker credentials are stored in ~/.docker/config.json. The configuration file should not contain hardcoded passwords but should use tokens or certificate-based authentication.

This page was rewritten on 15 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.