Tech Master Hub

Docker Security Best Practices

October 31, 2026 · 9 min read

Docker Security Best Practices: Securing Your Containerized Workloads

Docker has revolutionized how we build, ship, and run applications, but containerization introduces a unique security surface area that demands careful attention. A misconfigured container can expose sensitive data, enable privilege escalation, or become a pivot point for broader infrastructure compromise. This article covers practical, actionable security best practices for Docker environments — from image construction to runtime hardening — so you can ship fast without sacrificing safety.


1. Minimize the Attack Surface with Slim Base Images

The smaller your base image, the fewer vulnerabilities it contains. Large images like ubuntu or centos carry thousands of packages — many of which are unnecessary and potentially vulnerable.

Prefer Alpine-based or distroless images whenever possible. Distroless images from Google contain only your application and its runtime dependencies, stripping away package managers, shells, and utilities that attackers commonly leverage.

# ❌ Avoid: bulky base image
FROM ubuntu:22.04

# ✅ Prefer: minimal base image
FROM alpine:3.19

# ✅ Best: distroless for production
FROM gcr.io/distroless/base-debian12

When using alpine, be aware that it uses musl libc instead of glibc. Some applications may require additional compatibility layers or should be tested thoroughly before switching.


2. Build Secure Images with Multi-Stage Builds

Multi-stage builds let you compile and build artifacts in one image while shipping only the final, stripped-down result. This eliminates build tools, compilers, and source code from the production image entirely.

# Build stage
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o myapp .

# Production stage
FROM alpine:3.19
RUN apk add --no-cache ca-certificates tzdata
COPY --from=builder /app/myapp /myapp
RUN chmod +x /myapp
USER nobody
ENTRYPOINT ["/myapp"]

The resulting image contains only the compiled binary, CA certificates, and timezone data — no Go toolchain, no source files, no package manager.


3. Never Run Containers as Root

Running processes inside a container as root is one of the most common and dangerous misconfigurations. If an attacker escapes the container, they gain root access on the host. Always switch to a non-root user.

FROM node:20-alpine

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

# Create a non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

USER nodejs
COPY --chown=nodejs:nodejs . .

EXPOSE 3000
CMD ["node", "index.js"]

Additionally, enforce this at the daemon level by adding the following to /etc/docker/daemon.json:

{
  "no-new-privileges": true
}

This prevents any container process from gaining additional privileges through setuid or setgid binaries. You can also use a seccomp profile to restrict system calls:

docker run --security-opt seccomp=/path/to/profile.json myapp

4. Keep Images Updated and Scan Regularly

Vulnerabilities are discovered constantly. An image built today may be compromised tomorrow. Establish a routine of scanning your images and updating base images on a regular cadence.

Automated Scanning in CI/CD

Integrate scanning directly into your pipeline so vulnerable images never reach production.

# GitHub Actions example
name: Docker Security Scan
on: [push]

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - name: Build image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Run Trivy scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: myapp:${{ github.sha }}
          format: 'sarif'
          exit-code: '1'
          severity: 'HIGH,CRITICAL'

Recommended Scanning Tools

ToolTypeNotes
TrivyImage & config scannerFast, supports Kubernetes, IaC, and secrets
GrypeImage scanner (Syft)Great SBOM generation alongside scanning
DiveImage inspectorVisualize each layer to spot bloat and issues
ClairVulnerability classifierWorks well with CVE databases in large fleets

5. Protect Sensitive Data with Build Secrets and Runtime Volumes

Hardcoding credentials, API keys, or certificates inside an image layer is a critical mistake. Image layers are cached and often shared across registries, meaning secrets can leak indefinitely.

Build Secrets (BuildKit)

# syntax=docker/dockerfile:1
FROM alpine:3.19

RUN apk add --no-cache curl

# Securely inject secrets at build time without persisting them
RUN --mount=type=secret,id=token \
    TOKEN=$(cat /run/secrets/token) \
    curl -H "Authorization: Bearer $TOKEN" \
    https://api.example.com/config -o /etc/app/config.json

SECRET token

Build with:

DOCKER_BUILDKIT=1 docker build \
  --secret id=token,src=.secrets/token \
  -t myapp .

Runtime Secrets via Mounts

Never bake secrets into images. Instead, inject them at runtime through mounted volumes or orchestration-native secret stores.

# Mount a secret file at runtime
docker run \
  --secret my-db-password \
  myapp

# Or use environment variables from a secured source
docker run \
  -e DB_PASSWORD_FILE=/run/secrets/db_pass \
  myapp

For Kubernetes deployments, leverage native Secret objects with encryption at rest and RBAC-controlled access.


6. Use Read-Only Filesystems and Immutable Containers

Making container filesystems read-only prevents attackers from writing malicious scripts, modifying configurations, or establishing persistence inside a running container.

# docker-compose.yml
services:
  app:
    image: myapp:latest
    read_only: true
    tmpfs:
      - /tmp
      - /runtime
    volumes:
      - app-data:/var/data
      - ./config.json:/etc/app/config.json:ro

volumes:
  app-data:
// docker run flags equivalent
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--tmpfs /runtime:rw,noexec,nosuid,size=32m

Key points:

  • Mount only the directories that need write access.
  • Use noexec on temporary mounts to prevent executing binaries from those paths.
  • Mount configuration files as read-only (:ro) to prevent in-container modification.

7. Restrict Network Access and Isolate Containers

Containers share the host kernel, making network isolation essential. By default, Docker allows broad network communication between containers and the host.

Define Custom Networks

# docker-compose.yml
services:
  frontend:
    image: myapp-frontend:latest
    networks:
      - frontend-net

  backend:
    image: myapp-backend:latest
    networks:
      - frontend-net
      - backend-net

  database:
    image: postgres:16-alpine
    networks:
      - backend-net
    ports:
      - "5432:5432"  # Only accessible within backend-net

networks:
  frontend-net:
    driver: bridge
  backend-net:
    driver: bridge

The frontend service cannot reach the database directly — traffic must flow through the backend service, enforcing a clear network segmentation.

Disable Host Network Sharing

Never use network_mode: host unless absolutely necessary. It bypasses Docker's network isolation entirely, giving the container full access to the host's network stack.


8. Harden the Docker Daemon and Host OS

Container security starts at the host level. A compromised host means compromised containers, regardless of how well-secured the images are.

Daemon Configuration

Edit /etc/docker/daemon.json to enforce security policies:

{
  "icc": false,
  "no-new-privileges": true,
  "userns-remap": "default",
  "default-runtime": "runc",
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "live-restore": true
}
SettingPurpose
icc: falseDisables unrestricted inter-container communication
userns-remapMaps container root to a non-privileged host user
no-new-privilegesBlocks privilege escalation inside containers
live-restoreKeeps containers running during daemon restarts

User Namespace Remapping

User namespace remapping translates the container's root (UID 0) to an unprivileged UID on the host, dramatically reducing the impact of a container escape.

{
  "userns-remap": "default"
}

After enabling this, Docker automatically creates a dockremap user on the host and remaps all container UIDs. Note that this requires adjusting volume mount permissions accordingly.

Keep the Host Updated

Apply kernel updates promptly. Container escapes often exploit kernel vulnerabilities, so keeping the host OS patched is one of the highest-impact security actions you can take.


9. Enforce Resource Limits and Avoid DoS

Unrestricted resource consumption can lead to denial-of-service conditions affecting the entire host or co-located workloads. Always define limits.

# docker-compose.yml
services:
  app:
    image: myapp:latest
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
          pids: 256
        reservations:
          memory: 256M
    stop_grace_period: 30s

Or via CLI:

docker run \
  --cpus=1.0 \
  --memory=512m \
  --pids-limit=256 \
  --restart=unless-stopped \
  myapp

Key limits to consider:

  • CPU and memory prevent resource starvation of other workloads.
  • PID limits mitigate fork-bomb attacks.
  • Restart policies ensure availability without manual intervention.

10. Implement Runtime Monitoring and Logging

Detection is as important as prevention. Real-time monitoring catches anomalies — unexpected process launches, abnormal network connections, or filesystem changes — before they escalate.

Structured Logging with Rotation

Configure log drivers to send structured output to a centralized logging backend rather than letting logs accumulate locally.

// daemon.json
{
  "log-driver": "fluentd",
  "log-opts": {
    "fluentd-address": "localhost:24224",
    "fluentd-async": "true",
    "tag": "docker.{{.Name}}"
  }
}

Runtime Security Tools

ToolCapability
FalcoRuntime threat detection using system call auditing
Aqua MicroScannerLightweight runtime vulnerability scanning
SysdigContainer behavior analysis and compliance
auditdHost-level audit framework for filesystem and syscall monitoring

Example Falco rule for detecting unexpected shell access:

- rule: Shell spawned in container
  desc: Alert when a shell process starts inside a container
  condition: >
    spawned_process and container and
    shell_procs and proc.name != tsa
  output: >
    Shell spawned in container (
    user=%user.name container=%container.id
    host=%host.name command=%proc.cmdline
    parent=%proc.pname)
  priority: CRITICAL

11. Supply Chain Security and Image Trust

Compromised base images or malicious dependencies in the supply chain are a growing threat. Verify the provenance of every image you pull.

Sign and Verify Images with Notary/Cosign

# Sign an image with Cosign
cosign sign --key cosign.key myregistry.io/myapp:1.0.0

# Verify the signature before deployment
cosign verify --key cosign.pub myregistry.io/myapp:1.0.0

Use Private Registries with Access Control

Host a private registry (e.g., Harbor, GitLab Container Registry) and restrict who can push and pull images. Combine this with image scanning at the registry level so that unscanned images cannot be promoted to production.

# Restrict image sources with kubelet admission
# Pod security policy or OPA/Gatekeeper constraint
apiVersion: constraints.policy.cert-manager.io/v1
kind: ImagePolicy
spec:
  exemptImages: []
  allowRegistries:
    - "myregistry.io"
    - "gcr.io/distroless"
  denyRegistries:
    - "docker.io"

Pin Image Versions

Never use :latest in production. Pin images to specific digests or semantic versions.

# ✅ Pinned by digest — immutable and reproducible
FROM alpine:3.19.1@sha256:a1f6d5d...
FROM myapp:2.4.1@sha256:b2c8e7f...

# ❌ Never do this in production
FROM myapp:latest

12. Leverage Security Profiles and Standards

Standards like CIS Docker Benchmark and NSA/CISA Kubernetes Hardening Guide provide structured checklists for securing containerized environments. Aligning your configurations against these frameworks ensures coverage of known attack vectors.

Key CIS controls to implement:

  1. Ensure AppArmor/SELinux profiles are enabled — confine container processes.
  2. Disable privileged containers — privileged mode grants near-full host access.
  3. Set --cap-drop=ALL and add only required capabilities — follow least-privilege.
  4. Restrict memory swapping — prevent information leakage via swap.
  5. Enable container event logging — track container lifecycle for forensic analysis.
# Example: drop all capabilities, add only what's needed
docker run \
  --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \
  myapp

Conclusion

Securing Docker environments is not a one-time task — it's a layered, ongoing discipline that spans image construction, registry governance, runtime enforcement, and continuous monitoring. The most effective strategy combines defense in depth with automation: scan images in CI/CD, enforce policies at the daemon and cluster level, run workloads as non-root with minimal privileges, and monitor for anomalies in real time.

Start with the highest-impact practices — **minimal base images, non-root users, and

Latest posts