Docker containers are not sandboxes. A container running as root with the default configuration has a weaker isolation boundary than many developers assume. This guide covers practical Docker security for developers who build and ship containerized applications.
Build Secure Images
Use Minimal Base Images
Start with the smallest base image that works:
# Best: distroless (no shell, no package manager)
FROM gcr.io/distroless/static-debian12
# Good: Alpine-based (small attack surface)
FROM node:20-alpine
# Acceptable: slim variants
FROM python:3.12-slim
# Avoid: full OS images
# FROM ubuntu:22.04
Distroless images contain only your application and its runtime dependencies. No shell, no package manager, no unnecessary utilities. This dramatically reduces the attack surface.
Multi-Stage Builds
Separate build dependencies from runtime:
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Runtime stage
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["dist/server.js"]
The runtime image does not contain npm, build tools, source code, or dev dependencies.
Run as Non-Root
Never run containers as root:
FROM node:20-alpine
RUN addgroup -g 1001 -S appgroup && \
adduser -u 1001 -S appuser -G appgroup
WORKDIR /app
COPY --chown=appuser:appgroup . .
USER appuser
CMD ["node", "server.js"]
Verify your container is not running as root:
docker run --rm your-image whoami
# Should NOT output "root"
Pin Base Image Digests
Tags are mutable. Use SHA256 digests for reproducible builds:
FROM node:20-alpine@sha256:abc123def456...
This ensures you get exactly the same base image every time, even if the 20-alpine tag is updated.
Minimize Layers and Clean Up
Each Dockerfile instruction creates a layer. Combine commands and clean up in the same layer:
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
curl && \
rm -rf /var/lib/apt/lists/*
Do not leave package manager caches, build artifacts, or temporary files in your image.
Image Scanning
Trivy
Trivy is the most popular open-source container scanner:
trivy image your-image:latest
Run Trivy in CI to block builds with critical vulnerabilities:
trivy image --exit-code 1 --severity CRITICAL,HIGH your-image:latest
Docker Scout
Docker's built-in scanning tool:
docker scout cves your-image:latest
Grype
Another excellent open-source scanner:
grype your-image:latest
Run at least one scanner in your CI pipeline. Many teams run two for better coverage.
Runtime Security
Read-Only Root Filesystem
Run containers with a read-only root filesystem:
docker run --read-only --tmpfs /tmp your-image
In Docker Compose:
services:
app:
image: your-image
read_only: true
tmpfs:
- /tmp
This prevents malware from writing to the container filesystem.
Drop Capabilities
Containers inherit a set of Linux capabilities. Drop all and add only what you need:
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE your-image
In Docker Compose:
services:
app:
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
No Privileged Mode
Never use --privileged in production. It gives the container full access to the host:
# NEVER do this in production
docker run --privileged your-image
Resource Limits
Prevent resource exhaustion attacks:
services:
app:
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M
Network Isolation
Do not expose ports you do not need:
services:
app:
ports:
- "8080:8080"
db:
# No ports exposed to host
expose:
- "5432"
Use Docker networks to isolate services:
networks:
frontend:
backend:
services:
web:
networks: [frontend, backend]
api:
networks: [backend]
db:
networks: [backend]
Secrets Management
Never put secrets in environment variables in your Dockerfile or docker-compose.yml:
# NEVER do this
ENV DATABASE_PASSWORD=supersecret
Use Docker secrets or an external secrets manager:
services:
app:
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
Better: use a secrets manager like HashiCorp Vault, AWS Secrets Manager, or Doppler.
.dockerignore
Always include a .dockerignore file to prevent sensitive files from entering your image:
.git
.env
.env.*
*.pem
*.key
node_modules
__pycache__
.aws
credentials.json
SBOM Generation
Generate SBOMs for your container images:
# Using Trivy
trivy image --format cyclonedx --output sbom.json your-image:latest
# Using Syft
syft your-image:latest -o cyclonedx-json > sbom.json
How Safeguard.sh Helps
Safeguard.sh integrates with your container build pipeline to ingest SBOMs from every image. It tracks OS-level packages, language dependencies, and base image versions across your entire container fleet. When a CVE hits an Alpine package or a Python library bundled in your images, Safeguard.sh shows you exactly which containers are affected. It provides the fleet-wide visibility that individual image scans in isolated pipelines cannot deliver.