Loading...
All Articles
Security · 6 min read

Docker Security Hardening: 10 Essential Practices

A comprehensive guide to securing Docker containers in production, covering image scanning, runtime protection, secrets management, and more.

Why Container Security Cannot Be an Afterthought

Containers provide isolation, but they are not a security boundary by default. A misconfigured container can expose host filesystems, leak credentials, or provide an attacker with a foothold to move laterally across your infrastructure. The shared kernel model means a container escape affects every workload on the host.

At DevOpsVibe, we audit container environments regularly. These are the ten practices that make the biggest difference between a vulnerable deployment and a hardened one.

1. Use Minimal Base Images

Every package in your image is a potential attack surface. Start with the smallest image that works:

Before (vulnerable):

FROM ubuntu:22.04
RUN apt-get update && apt-get install -y python3 python3-pip curl wget vim
COPY . /app
RUN pip3 install -r /app/requirements.txt
CMD ["python3", "/app/main.py"]

After (hardened):

FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

FROM gcr.io/distroless/python3-debian12
COPY --from=builder /install /usr/local
COPY . /app
WORKDIR /app
CMD ["main.py"]

The distroless image has no shell, no package manager, and no unnecessary utilities. If an attacker gains code execution, there is almost nothing to work with.

Image size comparison:

  • ubuntu:22.04 with dependencies: ~450MB
  • python:3.12-slim multi-stage with distroless: ~85MB

2. Never Run as Root

By default, Docker runs processes as root inside the container. This is dangerous because a container escape gives the attacker root on the host:

FROM node:20-slim

# Create a non-root user
RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser

WORKDIR /app
COPY --chown=appuser:appuser . .
RUN npm ci --only=production

USER appuser
EXPOSE 3000
CMD ["node", "server.js"]

Verify at runtime:

docker run --rm my-app whoami
# Output: appuser

3. Scan Images for Vulnerabilities

Integrate image scanning into your CI/CD pipeline. We recommend Trivy for its speed and comprehensive database:

# Scan during build
trivy image --severity HIGH,CRITICAL --exit-code 1 my-app:latest

# Example output:
# my-app:latest (debian 12.4)
# Total: 0 (HIGH: 0, CRITICAL: 0)

Automate this in your pipeline:

# .github/workflows/security.yml
- name: Scan image
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: ${{ env.IMAGE }}
    format: sarif
    output: trivy-results.sarif
    severity: HIGH,CRITICAL
    exit-code: "1"

- name: Upload scan results
  uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: trivy-results.sarif

Also scan your Dockerfiles for misconfigurations:

trivy config --severity HIGH,CRITICAL ./Dockerfile

4. Pin Image Digests, Not Just Tags

Tags are mutable. Someone can push a compromised image to python:3.12-slim and every build that pulls that tag gets the malicious version:

# Risky: tag can be overwritten
FROM python:3.12-slim

# Secure: digest is immutable
FROM python:3.12-slim@sha256:a3e58c29e4a3b692a57d0e68b4b9c4f2e63e547e1b3f0a8e93c2a4fb7d3e1c9a

Get the digest with:

docker inspect --format='{{index .RepoDigests 0}}' python:3.12-slim

5. Implement Read-Only Filesystems

Prevent runtime modification of the container filesystem. If your application needs to write, mount specific writable volumes:

docker run --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,size=64m \
  --volume /app/data:/data:rw \
  my-app:latest

In Kubernetes:

securityContext:
  readOnlyRootFilesystem: true
  allowPrivilegeEscalation: false
  runAsNonRoot: true
  runAsUser: 1000
  capabilities:
    drop:
      - ALL

6. Manage Secrets Properly

Never bake secrets into images. They persist in layer history even if you delete them in a later layer:

Wrong:

# NEVER do this
ENV DATABASE_URL=postgres://admin:password@db:5432/myapp
COPY .env /app/.env

Right -- use build-time secrets (BuildKit):

# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=db_url \
    export DATABASE_URL=$(cat /run/secrets/db_url) && \
    python manage.py collectstatic
docker buildx build --secret id=db_url,src=./db_url.txt -t my-app .

At runtime, use your orchestrator's secrets management:

# Kubernetes secret injection
env:
  - name: DATABASE_URL
    valueFrom:
      secretKeyRef:
        name: api-secrets
        key: database-url

7. Limit Container Resources

An unbounded container can consume all host resources, creating a denial-of-service condition:

docker run \
  --memory=512m \
  --memory-swap=512m \
  --cpus=1.0 \
  --pids-limit=256 \
  --ulimit nofile=1024:1024 \
  my-app:latest

In Kubernetes, always set resource requests and limits:

resources:
  requests:
    memory: "256Mi"
    cpu: "250m"
  limits:
    memory: "512Mi"
    cpu: "1000m"

8. Drop All Capabilities

Linux capabilities give fine-grained control over what a process can do. Drop all of them and add back only what you need:

docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE my-app:latest

Common capabilities and when you actually need them:

CapabilityUse Case
NET_BIND_SERVICEBind to ports below 1024
CHOWNChange file ownership (rarely needed)
SYS_PTRACEDebugging only, never in production
SYS_ADMINAlmost never -- this is nearly equivalent to root

9. Use Multi-Stage Builds to Exclude Build Tools

Build dependencies like compilers, debuggers, and source code should never appear in your production image:

# Stage 1: Build
FROM golang:1.22 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server ./cmd/server

# Stage 2: Production
FROM scratch
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
ENTRYPOINT ["/server"]

The final image contains only the compiled binary and TLS certificates. No Go toolchain, no source code, no shell.

10. Enable Docker Content Trust and Sign Images

Ensure that only verified images run in your environment:

# Enable content trust
export DOCKER_CONTENT_TRUST=1

# Sign and push
docker trust sign myregistry.com/my-app:v1.2.3

# Verify
docker trust inspect myregistry.com/my-app:v1.2.3

For Kubernetes environments, use admission controllers like Kyverno or OPA Gatekeeper to enforce image policies:

# Kyverno policy: require signed images
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signatures
spec:
  validationFailureAction: Enforce
  rules:
    - name: verify-signature
      match:
        resources:
          kinds:
            - Pod
      verifyImages:
        - imageReferences:
            - "myregistry.com/*"
          attestors:
            - entries:
                - keys:
                    publicKeys: |-
                      -----BEGIN PUBLIC KEY-----
                      ...
                      -----END PUBLIC KEY-----

Security Checklist

Before deploying any container to production, verify:

  • Base image is minimal (distroless, Alpine, or scratch)
  • Image is scanned for CVEs with zero HIGH/CRITICAL findings
  • Container runs as non-root user
  • Filesystem is read-only where possible
  • No secrets baked into the image
  • Resource limits are set
  • All unnecessary capabilities are dropped
  • Image tags are pinned to digests
  • Images are signed and verified
  • Network policies restrict container communication

Conclusion

Container security is not a single tool or a one-time audit. It is a set of practices embedded into every stage of your pipeline -- from the Dockerfile to the runtime configuration. The ten practices above address the most common attack vectors we see in production environments.

At DevOpsVibe, we help teams build secure container pipelines from the ground up. Whether you need a security audit of your existing setup or want to implement DevSecOps practices across your organization, our team has the expertise to get it done right. Contact us to start the conversation.

filed under
dockerdockersecuritycontainersdevsecopshardeningkuberneteskubernetes
work with us

Want our team to help with your infrastructure?

talk to an engineerFree 30-min discovery callBook
close