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.
Deploy Your Own Tools — Recommended Hosting
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.