The Problem With Default Docker
Docker’s defaults prioritise ease of use. Out of the box, containers run as root, images are bloated with unnecessary tools, and the Docker daemon socket is world-accessible. In production, these defaults are serious liabilities.
1. Use a Non-Root User
Running as root inside a container means that a container escape grants root on the host.
FROM python:3.12-slim
# Create a low-privilege user and group
RUN groupadd --gid 1001 appgroup && \
useradd --uid 1001 --gid appgroup --no-create-home appuser
WORKDIR /app
COPY --chown=appuser:appgroup . .
RUN pip install --no-cache-dir -r requirements.txt
USER appuser
CMD ["python", "main.py"]
2. Use Minimal Base Images
Every package in your image is a potential vulnerability.
# BAD — 1.1 GB image with hundreds of CVEs
FROM ubuntu:22.04
# GOOD — distroless: no shell, no package manager, no CVEs from OS tools
FROM gcr.io/distroless/python3-debian12
# ALSO GOOD — Alpine: tiny, auditable
FROM python:3.12-alpine
Scan images before pushing:
# Trivy — fast, comprehensive scanner
trivy image myapp:latest
# Or with Docker Scout (built into Docker Desktop)
docker scout cves myapp:latest
3. Read-Only Root Filesystem
Prevent attackers from writing tools or modifying configs.
# docker-compose.yml
services:
app:
image: myapp:latest
read_only: true
tmpfs:
- /tmp # Allow writes only to tmpfs
- /var/run
Or in docker run:
docker run --read-only --tmpfs /tmp myapp:latest
4. Drop Capabilities
Linux capabilities are a fine-grained privilege model. Drop all, add only what you need.
# docker-compose.yml
services:
app:
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # Only if binding to port < 1024
5. Never Mount the Docker Socket
# NEVER DO THIS in production
volumes:
- /var/run/docker.sock:/var/run/docker.sock
Mounting the Docker socket gives a container full control over the Docker daemon — effectively root on the host. Use a dedicated CI/CD service account and the Docker API via TLS if remote access is required.
6. Seccomp & AppArmor Profiles
Docker ships with a default Seccomp profile. For hardened workloads, use a custom one.
# Apply a custom seccomp profile
docker run --security-opt seccomp=/path/to/profile.json myapp:latest
# Apply AppArmor profile
docker run --security-opt apparmor=docker-default myapp:latest
Generate a minimal profile with oci-seccomp-bpf-hook or syscall2seccomp.
7. Secret Management
Never bake secrets into images. Use Docker secrets or environment injection at runtime.
# BAD — secret baked into image layer
ENV DB_PASSWORD="supersecret123"
# GOOD — Docker secret (Swarm mode)
docker secret create db_password /path/to/secret.txt
# GOOD — Inject at runtime via orchestrator (Kubernetes Secret, AWS SSM, Vault)
Quick Security Checklist
| Control | Command / Config |
|---|---|
| Non-root user | USER 1001 in Dockerfile |
| Minimal base image | Use distroless or alpine |
| Image scanning | trivy image <tag> |
| Read-only FS | --read-only flag |
| Drop capabilities | cap_drop: [ALL] |
| No Docker socket mount | Remove volume binding |
| Secrets management | Docker secrets / Vault |
| Network isolation | Custom bridge networks, no --network host |
Automated Scanning in CI/CD
# GitHub Actions example
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:${{ github.sha }}'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1' # Fail the build on critical/high CVEs
Security gates in CI ensure you never deploy a vulnerable image.