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 --chownfor files baked into the image.Use named volumes or Kubernetes
fsGroupfor writable runtime directories.Write temporary files to
/tmpor 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
sudoor 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.