Docker Security Best Practices — Hardening Containers in Production

Harden your Docker containers with proven security best practices: minimal base images, non-root users, secrets management, and network isolation.

Why Container Security Matters

Containers share the host OS kernel, making security misconfigurations more impactful than in VMs. A compromised container can potentially escape to the host, pivot to other containers, or exfiltrate secrets mounted into the container filesystem.

The good news: most container breaches exploit basic misconfigurations rather than kernel exploits. Following the practices below eliminates the vast majority of attack surface.

Use Minimal Base Images

# Bad: full Ubuntu image (72MB+, hundreds of packages)
FROM ubuntu:22.04

# Better: official slim variant
FROM python:3.12-slim

# Best: distroless (no shell, no package manager)
FROM gcr.io/distroless/python3-debian12

Distroless images contain only the application runtime — no shell, no package manager, no utility binaries. This eliminates the tools an attacker would use after gaining code execution.

Run as Non-Root User

FROM python:3.12-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# Create a non-root user and switch to it
RUN addgroup --system app && adduser --system --ingroup app app
USER app

EXPOSE 8000
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Running as root means a container escape immediately grants root on the host. Non-root containers limit blast radius significantly.

Never Store Secrets in Images

# Bad: secret in ENV or ARG (visible in image layers)
ARG DATABASE_URL
ENV DATABASE_URL=$DATABASE_URL

# Good: pass secrets at runtime via environment
docker run -e DATABASE_URL=$DATABASE_URL myapp

# Better: use Docker secrets (Swarm) or mount from secret manager
docker run --secret id=db_url,src=./db_url.txt myapp

# In Dockerfile, access mounted secret without persisting it
RUN --mount=type=secret,id=db_url     cat /run/secrets/db_url > /tmp/config &&     pip install ... &&     rm /tmp/config

Limit Capabilities and Resources

# Drop all Linux capabilities, add only what's needed
docker run   --cap-drop=ALL   --cap-add=NET_BIND_SERVICE   --read-only   --tmpfs /tmp   --memory=512m   --cpus=1.0   --security-opt=no-new-privileges   myapp

Key flags: --read-only mounts the container filesystem as read-only. --no-new-privileges prevents privilege escalation via setuid binaries.

Scan Images for Vulnerabilities

# Trivy — fast, comprehensive vulnerability scanner
trivy image python:3.12-slim
trivy image --severity HIGH,CRITICAL myapp:latest

# Grype — alternative with SBOM support
grype myapp:latest

# In CI/CD (GitHub Actions)
- name: Scan image
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: myapp:${{ github.sha }}
    severity: HIGH,CRITICAL
    exit-code: 1

Frequently Asked Questions

Should I use Alpine Linux as a base image?

Alpine is popular for its small size (~5MB), but uses musl libc instead of glibc. Some Python packages with C extensions behave differently on musl. Distroless or -slim variants of official images are often safer choices.

How do I scan for secrets accidentally committed to a Dockerfile?

Use trufflehog or gitleaks in your CI pipeline to scan for API keys and tokens in the build context. Also add a .dockerignore to exclude .env files and ~/.ssh.

What is a rootless Docker daemon?

Running the Docker daemon as a non-root user (rootless mode) means even if a container escapes, the attacker does not have host root access. Enable with dockerd-rootless-setuptool.sh install.

🚀 Recommended: Deploy This on Hostinger VPS

The fastest way to get this running in production is a Hostinger VPS — starting at $3.99/mo, includes one-click Docker support, full root access, and SSD storage. Readers of this guide can use the link below for up to 75% off.

Get Hostinger VPS → Affiliate link — we may earn a commission at no extra cost to you.