← Docker articles

Docker / Image security

Smaller, Quieter, Safer: A Production Image Checklist

A practical checklist for multi-stage builds, least privilege, and predictable runtime behavior.

Shubham Anandani··5 min read

A production container should be small enough to move efficiently, quiet enough to operate, and constrained enough to limit avoidable risk. Those outcomes come from simple habits: keep build tools out of the runtime image, use a non-root identity, keep the build context tight, and make health checks cheap.

The aceu-trust-demo uses a multi-stage build, a slim base, a non-root user, a health endpoint, and Compose for local runs. The checklist below is designed to make those decisions repeatable.

1. Keep build tools out of the runtime stage

Compilers, headers, and test tools belong in a build stage when the running application does not need them. Copy only the installed runtime dependencies and application code into the final stage. A smaller image can reduce transfer time and remove packages the service does not need.

# syntax=docker/dockerfile:1
FROM python:3.12-slim AS builder
WORKDIR /app
RUN python -m venv /venv
ENV PATH="/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY aceu ./aceu

FROM python:3.12-slim AS runtime
RUN useradd --uid 10001 --create-home appuser
WORKDIR /app
COPY --from=builder --chown=10001:10001 /venv /venv
COPY --from=builder --chown=10001:10001 /app/aceu ./aceu
ENV PATH="/venv/bin:$PATH" \
    PYTHONDONTWRITEBYTECODE=1
USER 10001
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD python -c 'import urllib.request; urllib.request.urlopen("http://localhost:8000/health", timeout=2)' || exit 1
CMD ["uvicorn", "aceu.app:app", "--host", "0.0.0.0", "--port", "8000"]

Check the actual base image and application requirements before using this verbatim. For example, a health endpoint must exist and return a failing status when the process is unhealthy.

2. Choose a base image for the runtime

A minimal base can reduce unnecessary packages, but it may omit a system library your app needs. Add required runtime packages explicitly, then measure the result. Pick an image the team can patch and operate; “smallest possible” is not the only goal.

3. Run as a non-root user with a known UID

Use an explicit user ID and make sure files copied into the image are readable by that user. If a writeable directory is needed, grant access only to that path. Fix ownership in the Dockerfile instead of switching back to root when a permissions error appears.

4. Keep the build context narrow

Docker can only build from the context it receives. Exclude local caches, virtual environments, repository history, and secrets with a .dockerignore file:

__pycache__/
*.py[cod]
.env
.git/
node_modules/
venv/
.venv/
dist/
build/

Review COPY statements as carefully as ignore rules. Do not copy test fixtures or private configuration into the runtime image unless the application needs them.

5. Keep secrets out of image layers

Do not place credentials in Dockerfile ARG or ENV instructions, and do not copy secret files into a layer. Prefer platform-managed runtime secrets. When a build genuinely needs a secret, use BuildKit’s secret mount rather than baking it into the image.

6. Check for weakened isolation and noisy health checks

Review Dockerfiles and deployment manifests for settings that weaken isolation, such as an unconfined seccomp profile. Keep health checks bounded and inexpensive; an overly aggressive check can add load or mark a service unhealthy during a temporary resource spike. Set log levels intentionally so normal operation does not bury useful signals.

Trade-offs and failure modes

  • Slim image vs. hidden dependencies: add the libraries you need, then verify the runtime stage starts and serves its critical paths.
  • Non-root vs. file permissions: set ownership during build and grant write access only where needed.
  • Health checks vs. extra load: probe a fast path and use a reasonable timeout and interval.
  • Runtime secrets vs. rotation work: secure injection adds operational responsibilities, so document ownership and rotation clearly.

Common surprises include a credential accidentally left in an image layer, a slim runtime missing a required shared library, or a non-root process unable to read a root-owned file. A deliberate image review catches these before deployment.

Try it on your next image

  1. Separate build and runtime stages, then compare the resulting image size.
  2. Run the application under a non-root UID and verify file access.
  3. Add a .dockerignore file and inspect the build context.
  4. Add a CI lint step for known unsafe Dockerfile or manifest settings.

Closing thought

A smaller, quieter, safer image is easier to move and less likely to surprise the team in production. The demo remains illustrative; the container practices are the reusable part.