Container Security Best Practices
Containerization is not inherently secure. While it provides more isolation than a standard process, its shared-kernel architecture presents a unique set of security challenges. A "container escape," where a process breaks out of its isolated environment, gains access to the underlying host—and by extension, all other containers on that host.
A robust security posture requires a layered defense, applying the principle of least privilege at every stage of the container lifecycle.
1. Host and Daemon Security
Security starts with the host machine running the Docker daemon. If the host is compromised, all containers on it are compromised.
Keep the Host Patched: The host kernel is shared by all containers. A kernel vulnerability is the most critical threat, as it could be exploited by a container to gain root access to the host.
Use a Minimal Host OS: Use a container-optimized OS (like Fedora CoreOS, Bottlerocket OS, or Alpine). These "atomic hosts" have a minimal attack surface, fewer packages to exploit, and are often immutable.
Secure the Docker Daemon Socket: The Docker daemon socket (
/var/run/docker.sock) is root-equivalent. Any user or process that can access this socket has root-level control over the Docker host.Do not expose it over an unencrypted network port.
Do not add non-root users to the
dockergroup lightly; it is a privilege escalation risk.
Enable User Namespaces (
userns-remap): This is an advanced but powerful security feature. It maps therootuser inside the container (UID 0) to a non-privileged, high-numbered UID on the host. This means that even if an attacker breaks out of the container, they are a low-privilege user on the host, dramatically limiting their ability to do damage.
2. Image Security (Dockerfile Best Practices)
This is the developer's most critical responsibility. A secure container starts with a secure image.
Use Minimal Base Images
The Principle: Do not ship what you do not need. A large base image (like
ubuntu:latest) contains hundreds of packages, libraries, and utilities. Each one is a potential vulnerability.The Practice: Use minimal base images.
alpine: A very small, security-focused distribution.slim: Stripped-down versions of popular images (e.g.,python:3.10-slim).distroless: Google's "distroless" images contain only your application and its runtime dependencies. They do not contain package managers, shells, or any other utilities, making them extremely difficult to attack.
Run as a Non-Root User
The Principle: By default, containers run as
root(UID 0). This is a dangerous default.The Practice: Create a dedicated, non-root user in your
Dockerfileand switch to it using theUSERinstruction. A process running as a non-root user has significantly fewer privileges, mitigating the risk of container escape.
Example Dockerfile snippet:
# Create a dedicated group and user
RUN addgroup -S myappgroup && adduser -S myappuser -G myappgroup
# ... (install dependencies, copy code, etc.) ...
# Switch to the non-root user
USER myappuser
# The container will now run as 'myappuser'
CMD ["/app/start.sh"]Scan Your Images
The Principle: Your images, including their base layers, contain known vulnerabilities (CVEs). You must find and fix them.
The Practice: Integrate an image scanner into your CI/CD pipeline. These tools scan your image's layers, compare the package versions against a vulnerability database, and can be configured to fail the build if a critical vulnerability is found.
Tools:
Snyk,Trivy,Clair.
Use Multistage Builds
The Principle: Your final production image should not contain build tools (compilers, SDKs, dev dependencies) or source code.
The Practice: Use multistage builds (as detailed in File 07). The first stage (the "builder") installs all dev dependencies and builds the application. The final, minimal stage copies only the compiled binary or built artifact, leaving all build-time vulnerabilities behind.
Use .dockerignore
The Principle: The build context should not contain secrets, code history, or temporary files.
The Practice: Use a
.dockerignorefile to prevent sensitive files (.git,.env,id_rsa,*.log) from ever being sent to the Docker daemon or accidentally copied into the image.
3. Container Runtime Security
This is the "operator's" responsibility: how you run the container in production.
Grant "Least Privilege" with Capabilities
The Principle: The
rootuser's power is divided into "capabilities." By default, Docker drops most dangerous capabilities, but you can drop even more.The Practice:
Start by dropping all capabilities:
docker run --cap-drop=ALL ...Add back only the specific capabilities your application needs. For example, a web server that needs to bind to port 80 (a privileged port) needs
NET_BIND_SERVICE:docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE ...
Never Use --privileged
- The
--privilegedflag gives a container full root access to the host and all its devices. It effectively disables all isolation. It is meant for "container-in-container" or hardware-access scenarios and should be avoided.
Enforce Resource Limits
The Principle: A single compromised or poorly-coded container should not be able to exhaust all host resources (a Denial of Service attack).
The Practice: Use cgroup limits to constrain the container's resource usage.
--memory="2g": Set a hard memory limit.--cpus="1.5": Limit the container to 1.5 CPU cores.
Use a Read-Only Filesystem
The Principle: An attacker who gains access to a container will want to write to the filesystem (to install malware, deface files, etc.).
The Practice: Run the container with a read-only filesystem:
docker run --read-only ...How it works: This is a powerful defense. The application cannot write to its own filesystem. All data must be written to a dedicated Volume or an in-memory
tmpfsmount.docker run --read-only --mount type=tmpfs,destination=/tmp ...
Use Security Profiles
For advanced security, use the built-in Linux security modules. Docker supports them out of the box.
AppArmor or SELinux: These profiles provide mandatory access control (MAC), allowing you to define fine-grained rules for what a container is allowed to do (e.g., "this process can only read from
/var/wwwand bind to port 80").
4. Secrets Management
The #1 rule: Do not put secrets in images. Secrets (API keys, database passwords, certs) must be injected at runtime.
The Wrong Ways (Common Mistakes)
Environment Variables (
-eorENV): This is the most common mistake. Environment variables are not encrypted. They are stored in the image's configuration and are visible to anyone who can rundocker inspecton the container.Bind-Mounting Secret Files: This is better, but the secret file exists in plain text on the host, which may be insecure.
The Right Way: Use Secrets Management
Docker Secrets (for Swarm): This is the Docker-native solution. You create a secret (
docker secret create ...), and Docker makes it available to the container as a file in an in-memorytmpfsfilesystem at/run/secrets/<secret_name>. The secret never touches the container's or host's disk.Kubernetes Secrets: In Kubernetes, secrets are a first-class object, managed by the cluster and mounted into Pods (often as environment variables or
tmpfsvolumes).Vaults: For enterprise-grade secrets management, use a dedicated tool like HashiCorp Vault.
