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.
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.
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.
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.
If your container needs network binding:
docker run --cap-drop=ALL \
--cap-add=CAP_NET_BIND_SERVICE \
--read-only \
alpine:latest sh
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"
}
]
}
Avoid building containers with mutable base images. Pin image tags to specific digests:
docker run sha256:7d3d19b8c3f4a1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6 alpine:latest sh
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.
Control CPU and memory usage to prevent resource exhaustion attacks:
docker run --memory=128m --cpus="0.5" alpine:latest sh
Without memory limits, containers can consume all host RAM. This leads to OOM killer behavior that may terminate unrelated processes on the host.
When mounting volumes, ensure they are not writable by root inside the container:
docker run -v /host/path:/container/path:ro alpine:latest sh
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
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
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.
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.
Restrict network access to only necessary services:
docker run --network=none alpine:latest sh
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.
Scan images for known vulnerabilities before deployment:
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy image alpine:latest
Trivy's output includes severity levels, CVE identifiers, and affected packages. The scan results should be used to either:
Configure the Docker daemon to enforce security policies:
{
"userland-proxy": false,
"icc": false,
"userland-proxy": false,
"no-new-privileges": true
}
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.
Store secrets outside the container image:
docker run --env-file=/path/to/secrets.env alpine:latest sh
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.
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
The log driver configuration prevents disk space exhaustion from excessive logging. When logs exceed the maximum size, Docker rotates them automatically.
Implement proper cleanup procedures for container termination:
docker run --stop-timeout=30 alpine:latest sh
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.
Sign container images to ensure integrity:
docker run --pull=always gcr.io/distroless/base:latest
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.
When using container orchestrators, define network policies:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
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.
Use runtime-specific features for additional isolation:
docker run --cgroup-parent=/containers/myapp \
--memory-swap=256m \
--oom-score-adjust=-500 \
alpine:latest sh
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.
Test container configurations with runtime checks:
docker run --rm alpine:latest sh -c 'id && cat /proc/self/status'
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.
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
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.
Configure registry authentication to prevent unauthorized image pulls:
docker login registry.example.com
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.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy