Key Takeaways

  • Never run containers as root; use non-root users.
  • Scan images for vulnerabilities before deployment.
  • Use minimal base images (Alpine, distroless).
  • Never bake secrets into images; use secrets management.
  • Limit container capabilities and resources.
  • Use read-only file systems where possible.

1. Docker Security Overview

Containers share the host kernel, making security critical. A compromised container can potentially affect the host system and other containers. Docker security requires attention at multiple layers: the host, daemon, images, and running containers.

Key security principles for containers include: defense in depth, least privilege, immutable infrastructure, and regular updates. Apply these at every layer of your container stack.

Container Isolation

Containers use Linux namespaces (process, network, user, mount) and cgroups for isolation. This is lighter than VMs but provides less isolation. For high-security workloads, consider VM-based container runtimes like Kata Containers.

2. Image Security

2.1 Use Official and Verified Images

Start from official or verified publisher images. Check Docker Hub for the "Official Image" or "Verified Publisher" badges. Pin to specific versions, not :latest.

# Good - specific version
FROM node:20.10-alpine

# Avoid - mutable tags
FROM node:latest
FROM node:20

2.2 Minimal Base Images

Base ImageSizePackagesUse Case
ubuntu~77MBManyDevelopment, debugging
alpine~5MBMinimalMost production workloads
distroless~2MBNoneMaximum security
scratch0MBNoneStatic binaries only

3. Secure Dockerfile Practices

# Secure Dockerfile Example
FROM node:20-alpine AS builder

# Don't run as root
RUN addgroup -g 1001 -S appgroup && \
    adduser -S appuser -u 1001 -G appgroup

WORKDIR /app
COPY package*.json ./
RUN npm ci --production

FROM node:20-alpine

# Copy user from builder
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group

WORKDIR /app

# Copy app files with correct ownership
COPY --chown=appuser:appgroup --from=builder /app/node_modules ./node_modules
COPY --chown=appuser:appgroup . .

# Switch to non-root user
USER appuser

# Use exec form for proper signal handling
CMD ["node", "server.js"]

# Health check
HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget --quiet --tries=1 --spider http://localhost:3000/health || exit 1

3.1 Multi-Stage Builds

Multi-stage builds reduce image size and remove build tools from the final image:

FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o app

FROM gcr.io/distroless/static
COPY --from=builder /app/app /
CMD ["/app"]
Never Do This

ENV API_KEY=secret123 - Secrets in ENV are visible in image history
COPY .env /app - Don't copy secret files into images
RUN curl ... | bash - Installing unknown scripts

4. Runtime Security

4.1 Drop Capabilities

# Run with minimal capabilities
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE myapp

# Read-only filesystem
docker run --read-only myapp

# Limit resources
docker run --memory=512m --cpus=1 myapp

# No new privileges
docker run --security-opt=no-new-privileges myapp

4.2 Security Options

# Docker Compose security settings
services:
  app:
    image: myapp
    user: "1001:1001"
    read_only: true
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE
    security_opt:
      - no-new-privileges:true
    tmpfs:
      - /tmp
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '1.0'

5. Secrets Management

5.1 Docker Secrets (Swarm)

# Create secret
echo "my_password" | docker secret create db_password -

# Use in service
docker service create --secret db_password myapp

# Access in container: /run/secrets/db_password

5.2 Environment Variables (Non-Sensitive)

# Pass at runtime, not build time
docker run -e DATABASE_URL=$DATABASE_URL myapp

# Use .env file (not committed to git)
docker run --env-file .env myapp

5.3 External Secrets Management

For production, use HashiCorp Vault, AWS Secrets Manager, or similar tools. Containers fetch secrets at runtime rather than baking them in.

6. Network Security

# Create isolated network
docker network create --driver bridge --internal internal-net

# Only expose necessary ports
docker run -p 127.0.0.1:3000:3000 myapp  # localhost only

# Network isolation in Compose
networks:
  frontend:
  backend:
    internal: true  # No external access

services:
  web:
    networks: [frontend, backend]
  db:
    networks: [backend]  # Only internal access

7. Vulnerability Scanning

# Docker native scanning
docker scan myimage:latest

# Trivy (open source)
trivy image myimage:latest

# Snyk
snyk container test myimage:latest

# Scan in CI/CD
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy image --exit-code 1 --severity HIGH,CRITICAL myimage
Continuous Scanning

Scan images in CI/CD pipelines before deployment. Set up automated scanning of running containers. Subscribe to CVE alerts for your base images. Rebuild and redeploy when critical vulnerabilities are found.

8. Frequently Asked Questions

Are containers as secure as VMs?
No. Containers share the host kernel, providing less isolation than VMs. A kernel exploit can affect all containers. For high-security needs, consider VM-based containers (Kata, Firecracker) or traditional VMs.
Should I use Alpine images?
Alpine is excellent for reducing attack surface due to its minimal size. However, it uses musl libc which can cause compatibility issues with some applications. Test thoroughly if switching from glibc-based images.

Conclusion

Docker security requires attention at every layer: secure base images, hardened Dockerfiles, runtime restrictions, secrets management, and continuous scanning. Apply the principle of least privilege throughout, and treat containers as immutable infrastructure that gets rebuilt rather than patched.

Continue Learning:
Kubernetes Security Linux Security