Docker Production Hardening: From Image Scanning to Runtime Protection

This guide walks through a comprehensive Docker security hardening process for production, covering image vulnerability scanning, minimal base images, signed images, secure Dockerfile practices, daemon hardening, runtime privilege reduction, network isolation, secret management, monitoring, and a checklist to ensure continuous protection.

MaGe Linux Operations
MaGe Linux Operations
MaGe Linux Operations
Docker Production Hardening: From Image Scanning to Runtime Protection

Background and Scope

Docker is now the de‑facto standard for deploying services, but it brings a range of security risks such as vulnerable base images, misconfigurations, key leakage, container escape, and supply‑chain attacks. The article presents a layered hardening approach that spans image, build, runtime, network, and host layers.

Docker Image Security

Use Minimal Base Images

Avoid full‑distribution images like ubuntu:22.04 or debian:bookworm. Prefer Alpine, distroless, or other trimmed images, and use multi‑stage builds to keep the final image minimal.

# Not recommended: full OS images
FROM ubuntu:22.04
FROM debian:bookworm
FROM python:3.11

# Recommended: Alpine or distroless
FROM alpine:3.19
FROM python:3.11-alpine
FROM gcr.io/distroless/static-debian12

# Multi‑stage example
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp

FROM alpine:3.19
COPY --from=builder /app/myapp /myapp
ENTRYPOINT ["/myapp"]

Regular Vulnerability Scanning

Use open‑source scanners such as Trivy, Docker Scout, or Grype. Integrate scans into CI/CD pipelines to fail builds on high‑severity findings.

# Trivy example
wget https://github.com/aquasecurity/trivy/releases/download/v0.53.0/trivy_0.53.0_Linux-64bit.tar.gz
tar -xzf trivy_0.53.0_Linux-64bit.tar.gz
mv trivy /usr/local/bin

trivy image alpine:3.19
trivy image --severity HIGH,CRITICAL nginx:1.25
trivy image -f json --output result.json myregistry/myapp:latest

# Docker Scout
docker scout cves myregistry/myapp:latest
docker scout recommendations myregistry/myapp:latest

# Grype
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock anchore/grype:latest myregistry/myapp:latest

# CI/CD snippet (GitLab)
scan:
  stage: security
  script:
    - trivy image --exit-code 1 --severity HIGH,CRITICAL $IMAGE_TAG
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

Image Signing and Content Trust

Enable Docker Content Trust or use Cosign for modern signing.

# Docker Content Trust
export DOCKER_CONTENT_TRUST=1
export DOCKER_CONTENT_TRUST_SERVER_PASSWORD=xxx
export DOCKER_CONTENT_TRUST_SERVER_PIN=xxx

docker pull myregistry/myapp:latest

# Cosign signing
wget https://github.com/sigstore/cosign/releases/download/v2.2.0/cosign-linux-amd64
mv cosign-linux-amd64 /usr/local/bin/cosign
chmod +x /usr/local/bin/cosign
cosign sign --key cosign.key myregistry/myapp:latest
cosign verify --key cosign.pub myregistry/myapp:latest

Dockerfile Secure Build

Proper Secret Handling

Never hard‑code secrets in the Dockerfile. Inject them at runtime via environment variables, mounted files, or secret‑management services.

# Bad example (hard‑coded key)
FROM alpine:3.19
ENV API_KEY=sk-xxxxxxx   # ❌
COPY secret.key /app/secret.key   # ❌

# Good example (runtime injection)
FROM alpine:3.19
COPY dummy-key.pub /app/key.pub   # placeholder
# Run container with secret
docker run -e API_KEY=sk-xxxxxxx myapp
# Or mount a read‑only secret file
docker run -v /path/to/key:/app/key:ro myapp

# Multi‑stage secret usage
FROM golang:1.22-alpine AS builder
ARG API_KEY
ENV API_KEY=$API_KEY
RUN go build -ldflags="-X main.apiKey=$API_KEY" -o myapp

FROM alpine:3.19
COPY --from=builder /app/myapp /myapp
ENTRYPOINT ["/myapp"]

Least Privilege User

# Create non‑root user
RUN addgroup -g 1000 appgroup && adduser -u 1000 -G appgroup -s /bin/sh -D appuser
USER appuser

Other Dockerfile Recommendations

# Prefer COPY over ADD
COPY --chown=appuser:appgroup app /app

# Explicit port exposure
EXPOSE 8080

# Set working directory
WORKDIR /app

# Healthcheck
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

# Avoid latest tag
# Bad: FROM nginx:latest
# Good: FROM nginx:1.25.4-alpine

# Verify download integrity
RUN wget -O file.tar.gz https://example.com/file.tar.gz \
    && echo "sha256:abc123..." | sha256sum -c -

Docker Daemon Configuration

# /etc/docker/daemon.json
{
  "icc": false,
  "userns-remap": "default",
  "live-restore": true,
  "no-new-privileges": true,
  "default-ulimits": {
    "nofile": {"Name": "nofile", "Hard": 64000, "Soft": 64000}
  },
  "default-cgroupns-mode": "host",
  "storage-driver": "overlay2",
  "log-driver": "json-file",
  "log-opts": {"max-size": "100m", "max-file": "3"},
  "registry-mirrors": [],
  "insecure-registries": [],
  "userland-proxy": false,
  "fixed-cidr": "172.17.0.0/16"
}

# Apply changes
systemctl restart docker
# Verify
docker info | grep -E "Storage Driver|Userns|Logging"

Container Runtime Security

Minimize Container Privileges

# Do not use --privileged
# Bad: docker run --privileged myapp
# Good: docker run myapp

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

# Drop all capabilities, add only needed ones
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE myapp

# Prevent new privileges
docker run --security-opt=no-new-privileges:true myapp

# Disable SUID/SGID via seccomp profile
docker run --security-opt="seccomp=profile.json" myapp

# AppArmor / SELinux enforcement
docker run --security-opt apparmor=my-profile myapp
docker run --security-opt label=type:container_t myapp

Custom seccomp Profile

# Download default profile
curl -o /etc/docker/seccomp.json \
  https://raw.githubusercontent.com/moby/moby/master/profiles/seccomp/default.json

# Create a restrictive profile (allow only needed syscalls)
cat > /etc/docker/seccomp-app.json <<'EOF'
{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": ["SCMP_ARCH_X86_64","SCMP_ARCH_X86","SCMP_ARCH_ARM"],
  "syscalls": [{
    "names": ["accept","accept4","bind","close","read","write","execve"],
    "action": "SCMP_ACT_ALLOW"
  }]
}
EOF

# Run with custom profile
docker run --security-opt seccomp=/etc/docker/seccomp-app.json myapp

Resource Limits

# Memory limit
docker run -m 512m --memory-swap 1g myapp
# CPU limit
docker run --cpus 1.5 myapp
# PID limit (prevent fork bombs)
docker run --pids-limit 100 myapp
# I/O throttling
docker run --device-read-bps /dev/sda:10mb myapp
# Network bandwidth (requires traffic control)
docker run --network none myapp   # completely disable network

Key Management

# Bad: hard‑coded or env‑exposed keys
ENV API_KEY=sk-xxxxxxx
# Good: external secret stores
# Vault example
docker run -d --name=vault -p 8200:8200 \
  -e VAULT_ADDR=http://localhost:8200 -e VAULT_TOKEN=xxx vault server -dev

vault kv put secret/myapp/api-key value=sk-xxxxxxx

# Retrieve in init container (K8s example)
apiVersion: v1
kind: Pod
metadata:
  name: myapp
spec:
  initContainers:
  - name: get-secrets
    image: vault:1.13
    env:
    - name: VAULT_ADDR
      value: "http://vault:8200"
    - name: VAULT_TOKEN
      valueFrom:
        secretKeyRef:
          name: vault-token
          key: token
    command: ["sh","-c","vault kv get -format=json secret/myapp/api-key | jq -r '.data.data.api_key' > /secrets/api_key']
    volumeMounts:
    - name: secrets
      mountPath: /secrets
  containers:
  - name: app
    image: myapp
    volumeMounts:
    - name: secrets
      mountPath: /secrets
      readOnly: true
  volumes:
  - name: secrets
    emptyDir:
      medium: Memory

Network Isolation

Network Mode Selection

# Default bridge (containers can talk to each other)
docker run --network bridge myapp

# Host mode (dangerous, not recommended)
docker run --network host myapp

# No network
docker run --network none myapp

# Custom bridge for isolation
docker network create --driver bridge mynetwork
docker run --network mynetwork myapp

# Example of front‑end / back‑end isolation
docker network create frontend
docker network create backend

docker run --network frontend --name web myweb
docker run --network backend --name db mydb
# Web cannot directly reach db unless explicitly allowed

Network Access Control

# iptables rule to block a specific IP from containers
iptables -I DOCKER -p tcp -d 10.0.0.1 -j DROP
# Block external internet, allow only internal subnet
iptables -I DOCKER -p tcp ! -s 172.17.0.0/16 -j DROP

# Docker‑Compose network isolation
version: "3.8"
services:
  app:
    image: myapp
    networks: [frontend]
  db:
    image: mysql:8.0
    networks: [backend]
networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true   # no outbound internet

Monitoring and Auditing

Log Auditing

# Configure JSON log driver with rotation
{
  "log-driver": "json-file",
  "log-opts": {"max-size": "100m", "max-file": "5"}
}

# View logs
docker logs mycontainer
docker logs --tail 100 mycontainer
docker logs --since 10m mycontainer

Centralised Log Collection (ELK/Loki)

# Filebeat input for Docker logs
filebeat.inputs:
- type: container
  paths:
    - /var/lib/docker/containers/*/*.log
  processors:
    - add_docker_metadata:
        host: "unix:///var/run/docker.sock"

Behavior Auditing with Falco

# Install Falco
curl -s https://download.falco.org/packages/rpm/stable/falco-0.37.0-x86_64.rpm | rpm -ivh -

# Example rule: detect unexpected shells
- rule: Terminal shell in container
  desc: A shell was spawned in a container other than allowed binaries
  condition: spawned_process and container and proc.name in (shell_binaries) and not proc.pname in (shell_binaries)
  output: Shell spawned in container (user=%user.name container_id=%container.id container_name=%container.name shell=%proc.name parent=%proc.pname cmdline=%proc.cmdline)
  priority: NOTICE

# Rule: unauthorized image
- rule: Unauthorized container image
  desc: Detected use of unauthorized image
  condition: container and container.image.repository != "myregistry/trusted-image"
  output: Unauthorized container image (container_id=%container.id image=%container.image.repository)
  priority: WARNING

Real‑time Metrics

# docker stats (interactive)
docker stats
# One‑shot view
docker stats --no-stream mycontainer
# Custom format
docker stats --format "table {{.Name}}	{{.CPUPerc}}	{{.MemUsage}}"

# cAdvisor container
docker run \
  --volume=/:/rootfs:ro \
  --volume=/var/run:/var/run:ro \
  --volume=/sys:/sys:ro \
  --volume=/var/lib/docker/:/var/lib/docker:ro \
  --publish=8080:8080 \
  gcr.io/cadvisor/cadvisor:latest

# Prometheus scrape config for cAdvisor
global:
  scrape_interval: 15s
scrape_configs:
  - job_name: 'docker'
    static_configs:
      - targets: ['cadvisor:8080']

Docker Security Baseline Checklist

# Image checks
[ ] Use minimal base image (alpine/distroless)
[ ] No "latest" tag
[ ] Scanned with no high‑severity CVEs
[ ] Image signature verified

# Build checks
[ ] Dockerfile contains no secrets
[ ] Non‑root user defined
[ ] Read‑only filesystem enforced
[ ] Cache and temporary files removed

# Runtime checks
[ ] Resource limits (CPU, memory) set
[ ] Network isolation configured
[ ] Privileged mode disabled
[ ] Capabilities minimized
[ ] seccomp/AppArmor profile applied

# Operations checks
[ ] Centralised log collection
[ ] Monitoring and alerting in place
[ ] Regular security scans integrated
[ ] Images updated regularly

Common Issues and Fixes

Issue 1 – Container Runs as Root

# Fix: create non‑root user and drop capabilities
RUN addgroup -g 1000 appgroup && adduser -u 1000 -G appgroup -s /bin/sh -D appuser
USER appuser

# Run with minimal capabilities
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE myapp
# Enforce read‑only rootfs
docker run --read-only myapp

Issue 2 – Secret Leakage

# Do not embed keys in images or env vars
# Use external secret stores (Vault, cloud KMS) or Docker/K8s secrets
docker secret create api_key api_key.txt
docker service create --secret api_key myapp
# Or mount read‑only secret file
docker run -v /secrets:/run/secrets:ro myapp
chmod 400 /secrets/api_key

Issue 3 – Container Escape Risk

# Avoid --privileged and shared host namespaces
# Enable user‑namespace remapping
{ "userns-remap": "default" }
# Disallow new privileges
docker run --security-opt=no-new-privileges:true myapp

Issue 4 – Image Vulnerabilities

# Switch to minimal image
FROM alpine:3.19
# Regularly pull latest base
docker pull alpine:3.19
docker build --pull .
# Use Distroless for production binaries
FROM gcr.io/distroless/static-debian12
# Fail CI on high‑severity CVEs
trivy image --exit-code 1 --severity HIGH,CRITICAL myapp

Conclusion

Docker security hardening must be approached as a defense‑in‑depth strategy. By securing the image layer, enforcing safe build practices, configuring a hardened daemon, limiting runtime privileges, isolating networks, managing secrets externally, and continuously monitoring and auditing, organizations can maintain a resilient container environment. Integrating these controls into CI/CD pipelines and performing regular baseline checks ensures long‑term protection.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

monitoringDockercontainer securityimage scanningnetwork isolationseccompsecret managementruntime hardening
MaGe Linux Operations
Written by

MaGe Linux Operations

Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.