Docker Security Best Practices
Why Docker Security Matters
Containers share the host OS kernel. A compromised container can potentially affect the host or other containers. Following security best practices reduces this risk.
1. Run as a Non-Root User
By default, processes inside containers run as root (user ID 0). This is dangerous - if an attacker escapes the container, they have root on the host.
In a Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm ci --only=production
# Create a non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Switch to non-root user
USER appuser
CMD ["node", "server.js"]
At Runtime
# Run as a specific user
docker run -d --user 1000:1000 my-app
# Run as a user by name
docker run -d --user appuser my-app
# Check what user a container is running as
docker exec my-app whoami
docker inspect -f '{{.Config.User}}' my-app
2. Use Read-Only Filesystems
Prevent the container from writing to its filesystem (except specific allowed paths).
# Run with read-only filesystem
docker run -d --read-only my-app
# Allow writes only to specific directories using tmpfs
docker run -d \
--read-only \
--tmpfs /tmp \
--tmpfs /var/run \
my-app
# Verify
docker inspect -f '{{.HostConfig.ReadonlyRootfs}}' my-app
In Docker Compose:
services:
app:
image: my-app
read_only: true
tmpfs:
- /tmp
- /var/run
3. Drop Unnecessary Linux Capabilities
Linux capabilities grant specific privileges. By default, containers get a set of capabilities. Drop all and only add what you need.
# Drop all capabilities
docker run -d --cap-drop ALL my-app
# Drop all, then add back only what's needed
docker run -d \
--cap-drop ALL \
--cap-add NET_BIND_SERVICE \ # allow binding to ports < 1024
my-app
# Common capabilities to add back
--cap-add NET_BIND_SERVICE # bind to privileged ports
--cap-add CHOWN # change file ownership
--cap-add SETUID # switch user ID
--cap-add SETGID # switch group ID
In Docker Compose:
services:
app:
image: my-app
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
4. Never Run Privileged Containers
--privileged gives the container almost all Linux capabilities and access to host devices. Avoid it.
# BAD - never do this unless absolutely required
docker run -d --privileged my-app
# GOOD - run without privileges
docker run -d my-app
5. Limit Resources
Prevent a compromised container from starving the host.
docker run -d \
--memory 512m \
--cpus 1 \
--pids-limit 100 \ # limit number of processes
my-app
6. Use Official and Verified Images
# Prefer official images
docker pull nginx # official
docker pull mysql:8.0 # official
# Avoid unknown community images for production
docker pull random-user/nginx # unknown source
# Always specify a version tag - never just "latest" in production
docker pull nginx:1.25 # specific version
docker pull nginx:latest # could change without warning
7. Scan Images for Vulnerabilities
# Scan an image with Docker Scout (built into Docker)
docker scout cves nginx
docker scout cves my-app:1.0
# Quickview
docker scout quickview nginx
# Show only critical and high vulnerabilities
docker scout cves --only-severity critical,high nginx
# Compare two images
docker scout compare my-app:1.0 my-app:2.0
8. Use .dockerignore
Prevent sensitive files from being included in the build context.
# .dockerignore
.env
.env.*
*.key
*.pem
*.p12
secrets/
credentials/
.git/
node_modules/
9. Don't Store Secrets in Images
# BAD - secret is baked into the image and visible in history
ENV API_KEY=abc123secretkey
# BAD - secret in RUN command (visible in image layers)
RUN echo "abc123" > /app/secret.txt
# GOOD - pass secrets at runtime
# docker run -e API_KEY=abc123 my-app
# GOOD - use Docker secrets (Swarm/Compose)
Check image history for exposed secrets:
docker image history --no-trunc my-app
10. Use Docker Secrets (Docker Compose/Swarm)
For sensitive data, use Docker secrets instead of environment variables.
# docker-compose.yml
services:
app:
image: my-app
secrets:
- db_password
- api_key
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
file: ./secrets/api_key.txt
Secrets are mounted as files at /run/secrets/<name> inside the container. They are never stored in environment variables or image layers.
11. Limit Container Network Access
# Deny all outgoing network (for batch jobs that don't need internet)
docker run -d --network none my-batch-job
# Use separate networks to isolate services
docker network create frontend-net
docker network create backend-net
# Database only on backend network (not accessible from frontend)
docker run -d --network backend-net mysql:8.0
# Web app on both networks (bridge between them)
docker run -d --network frontend-net --network backend-net my-app
12. Keep Images Up to Date
# Pull latest version of base images regularly
docker pull node:20-alpine
docker pull python:3.11-slim
# Rebuild your images after updating base
docker build --no-cache -t my-app:latest .
13. Use --no-new-privileges
Prevents container processes from gaining new privileges via setuid or setgid.
docker run -d --security-opt no-new-privileges my-app
In Docker Compose:
services:
app:
image: my-app
security_opt:
- no-new-privileges:true
14. Seccomp Profiles
Seccomp (Secure Computing Mode) filters which system calls a container can make.
# Docker applies a default seccomp profile automatically
# To run without seccomp (not recommended)
docker run -d --security-opt seccomp=unconfined my-app
# Apply a custom seccomp profile
docker run -d --security-opt seccomp=/path/to/profile.json my-app
Security Checklist
| Practice | Command / Method |
|---|---|
| Non-root user | USER appuser in Dockerfile, --user at runtime |
| Read-only filesystem | --read-only + --tmpfs |
| Drop capabilities | --cap-drop ALL --cap-add NEEDED |
| No privileged mode | Never use --privileged |
| Resource limits | --memory, --cpus, --pids-limit |
| Official images only | Use nginx, mysql, etc. |
| Pin image versions | nginx:1.25, not nginx:latest |
| Scan for vulnerabilities | docker scout cves my-app |
| No secrets in images | Pass via -e or Docker secrets |
| No new privileges | --security-opt no-new-privileges |
| Network isolation | Custom networks, --network none |
| Keep images updated | Regular rebuilds with --no-cache |
FAQ
Should I memorize every Docker command?+
No. Memorize the core workflow first: build, run, list, inspect, logs, exec, stop, remove, and clean up. Then learn specialized commands when you need them.
Is Docker only for developers?+
No. Docker is useful for system administrators, infrastructure engineers, DevOps engineers, cloud engineers, support engineers, and learners who want repeatable labs.
What should I do after reading this guide?+
Run the examples, write down what each command changes, rebuild the workflow with Docker Compose, and then add one CI/CD step that builds the image automatically.
Need help applying Docker in a real project?
Work directly with Muhammad Irfan Aslam for Docker, Linux, DevOps, CI/CD, cloud deployment, or infrastructure troubleshooting support.
Hire Me for Support