Image Hardening

Container security starts at image build time. A hardened image is smaller, has fewer packages, avoids embedded secrets, runs with least privilege, and is easy to rebuild when the base image changes.

Start from a small trusted base

Use official, vendor-supported, or organization-approved base images. Prefer a slim runtime base over a full development distribution.

1# syntax=docker/dockerfile:1
2FROM python:3-slim AS runtime

Pin versions for reproducibility. For high-control environments, pin the base image by digest and schedule automated digest refreshes.

1FROM python:3-slim@sha256:<digest>

Keep build tools out of runtime images

Use multi-stage builds so compilers, package caches, test fixtures, and source-control metadata do not land in the runtime image.

 1# syntax=docker/dockerfile:1
 2FROM python:3-slim AS build
 3WORKDIR /app
 4COPY requirements.txt .
 5RUN --mount=type=cache,target=/root/.cache/pip \
 6    pip wheel --wheel-dir /wheels -r requirements.txt
 7
 8FROM python:3-slim AS runtime
 9WORKDIR /app
10RUN useradd --create-home --uid 10001 appuser
11COPY --from=build /wheels /wheels
12RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels
13COPY --chown=appuser:appuser . .
14USER appuser
15EXPOSE 8080
16CMD ["python", "app.py"]

Run as a non-root user

Containers should not need root for normal application work. Create an application user and switch to it with USER. Install operating system packages as root during the build, then switch to the application user before copying application code or starting the process.

 1# syntax=docker/dockerfile:1
 2FROM python:3-slim
 3
 4RUN apt-get update -y && \
 5    apt-get install --no-install-recommends curl -y && \
 6    rm -rf /var/lib/apt/lists/*
 7
 8RUN groupadd --gid 10001 app && \
 9    useradd --uid 10001 --gid app --create-home app
10
11WORKDIR /app
12COPY --chown=app:app requirements.txt .
13RUN pip install --no-cache-dir -r requirements.txt
14COPY --chown=app:app . .
15
16USER 10001:10001
17CMD ["python", "app.py"]

Use a numeric UID and GID for runtime ownership. User names are convenient inside one image, but numeric IDs are what the kernel, volumes, Kubernetes security contexts, and host filesystems enforce.

Avoid sudo and personal admin accounts

Do not install sudo into application images and do not bake a personal superuser account such as super into the image. A container is not a small VM that needs interactive administration. Rebuild the image when system packages change, and use logs, metrics, shell debugging, or one-off diagnostic containers for troubleshooting.

Avoid this pattern.

1FROM ubuntu:latest
2RUN apt-get update && apt-get install sudo -y
3RUN useradd --create-home super && echo "super ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
4USER super
5CMD ["sudo", "python3", "app.py"]

Prefer this pattern.

1FROM python:3-slim
2RUN useradd --uid 10001 --create-home app
3WORKDIR /app
4COPY --chown=10001:10001 . .
5USER 10001:10001
6CMD ["python", "app.py"]

If an image truly needs a startup action as root, keep that action narrow and drop privileges before starting the long-running process. Most web applications, workers, batch jobs, and model servers do not need this.

Runtime controls

Also configure the runtime or orchestrator to drop Linux capabilities when possible.

 1services:
 2  api:
 3    image: example/api:dev
 4    user: "10001:10001"
 5    cap_drop:
 6      - ALL
 7    security_opt:
 8      - no-new-privileges:true
 9    read_only: true
10    tmpfs:
11      - /tmp

In Kubernetes, put the same policy in the Pod or container security context.

 1apiVersion: apps/v1
 2kind: Deployment
 3metadata:
 4  name: api
 5spec:
 6  template:
 7    spec:
 8      securityContext:
 9        runAsNonRoot: true
10        runAsUser: 10001
11        runAsGroup: 10001
12        fsGroup: 10001
13      containers:
14        - name: api
15          image: registry.example.com/team/api:latest
16          securityContext:
17            allowPrivilegeEscalation: false
18            readOnlyRootFilesystem: true
19            capabilities:
20              drop:
21                - ALL

Volume ownership

Non-root images often fail when a mounted volume is owned by root. Fix the ownership model rather than running the container as root.

  • Use COPY --chown for files baked into the image.

  • Use named volumes or Kubernetes fsGroup for writable runtime directories.

  • Write temporary files to /tmp or another explicit writable mount.

  • Avoid writing into the application directory at runtime.

  • Make logs go to stdout and stderr, not root-owned files inside the image.

Rootless Docker

Rootless Docker runs the Docker daemon and containers inside a user namespace without root privileges on the host. It reduces risk from daemon and runtime vulnerabilities. It does not replace image-level hardening: the application inside the image should still run as a non-root user, avoid sudo, and drop unnecessary runtime privileges.

Do not bake secrets into images

Do not copy .env files, cloud credentials, SSH keys, package tokens, or TLS private keys into images. Do not pass them as ARG or ENV in the Dockerfile. Use BuildKit secrets during the build and runtime secrets from the orchestrator.

Use a .dockerignore file

A tight build context prevents accidental data leaks and speeds up builds.

1.git
2.env
3.aws
4.ssh
5node_modules
6__pycache__
7*.pyc
8dist
9build

Add health metadata

Images can include a health check when the process has a cheap local check. Compose and orchestrators can override it when the environment needs a different probe.

1HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
2    CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health')"

Rebuild often

Images age as soon as their base image and packages age. Rebuild on a schedule, scan the result, and redeploy even when application code has not changed. A reproducible build pipeline makes patching a normal operation instead of an emergency.

Checklist

  • Use approved base images.

  • Prefer slim runtime images and multi-stage builds.

  • Pin dependencies and refresh them intentionally.

  • Create a dedicated runtime UID/GID and set USER.

  • Do not install sudo or create personal admin accounts in application images.

  • Do not rely on root for writable directories; fix ownership and mounts.

  • Drop capabilities and prevent privilege escalation at runtime.

  • Keep credentials out of Dockerfiles, build args, image layers, and logs.

  • Maintain .dockerignore.

  • Scan images and fail CI on policy violations.

  • Publish SBOM and provenance attestations for release images.

References