All resources
Cloud Run10 min read

Docker image hardening for production services

Reduce container attack surface with minimal images, non-root execution, pinned inputs, and runtime checks that survive deployment.

A practical PingFlow guide for developers working at the boundary between systems.

At a glance

Key takeaways

  • The image is part of the runtime boundary
  • Use multi-stage builds and pinned inputs
  • Run as a non-root identity
In this guide

The image is part of the runtime boundary

A container image carries the application and the operating-system libraries it uses. A vulnerable shell, certificate bundle, native parser, or debugging tool can create risk even when the application code is correct. Start by listing what the service needs at runtime and remove what it does not. A small image reduces patching work, startup transfer, and the number of tools an attacker can use after a compromise.

Hardening is not about making the image mysterious. It is about making its contents and behavior intentional. Use a documented base image, a lockfile, a reproducible build, and an image digest. Keep source and build tools out of the final stage. If a runtime library is required, know which package provides it and how it receives security updates.

Use multi-stage builds and pinned inputs

A builder stage can install compilers, package managers, and test dependencies, then copy only the standalone runtime output into a clean runner stage. This reduces size and prevents accidental inclusion of credentials or caches. Pin base images by a reviewed tag and record the resolved digest in the build record. A tag can move after approval, so promotion should use the immutable image digest.

Keep the build context small with a correct ignore file. Do not send local environment files, keys, dependency caches, or generated logs to the remote builder. Inspect the context contents and the final image layers. A secret deleted in a later layer can remain recoverable from an earlier layer, so never copy it into the build in the first place.

Run as a non-root identity

A process that runs as root has more authority inside the container and can make a host or runtime escape more damaging if another control fails. Create a dedicated user and group, set ownership only on directories the process must write, and use read-only paths for application code. Cloud Run and other managed runtimes add isolation, but least privilege still reduces the impact of application vulnerabilities.

Test the image as the final user, not only as a root builder. Temporary files, certificate stores, font directories, and native libraries often reveal permission assumptions. If the service needs a writable directory, give it a size and cleanup policy. Do not make the whole filesystem writable to avoid fixing one path.

Keep secrets out of layers and environment dumps

Build arguments, environment variables, package manager configuration, and shell history can leak credentials into image history or logs. Pass only public build values to a frontend build, and retrieve server secrets at runtime through a managed store. A secret that is needed during a build should come from a short-lived secret mount or a trusted build identity, never from a committed file.

Do not print the environment on startup or include it in diagnostics. Record only non-sensitive configuration fingerprints and secret version metadata. Scan the final image and build logs for high-entropy strings and known credential patterns. If a secret appears, rotate it before debating whether the layer is reachable.

Make signals, ports, and shutdown explicit

The container should listen on the platform-provided port, expose the expected health behavior, and handle termination signals. A process that ignores SIGTERM can lose requests during a revision rollout. Keep startup work bounded and report dependency failures without blocking readiness forever. Do not use a health endpoint that performs an expensive query on every probe.

Set resource limits and a non-root user in the runtime configuration. Monitor memory and file descriptors because a small image can still leak resources. If the service writes logs, use stdout or the platform's supported path and apply redaction before a payload reaches it.

Scan and attest before promotion

Scan the final image for operating-system and application vulnerabilities, licenses, and unexpected packages. A finding is a signal for triage, not a reason to ignore the scanner. Record the image digest, source revision, base image, dependency lockfile, builder identity, and scan result. Block promotion on policies that match the service's actual risk and assign owners for exceptions.

Sign or attest the artifact when the platform supports verification, and deploy the same digest that passed checks. Do not rebuild between scanning and production without rechecking. Keep a previous known-good digest for rollback. An image hardening program is only useful if the deployment path enforces the evidence.

Test the runtime, not just the Dockerfile

Run the image as its final user, with production-like environment and network restrictions. Test startup, shutdown, temporary storage, TLS, native dependencies, request timeouts, and a normal smoke suite. Inspect runtime processes and open files where the platform permits. Verify that a missing secret fails closed and that the application cannot write to its code directory.

A hardened image is small, reproducible, least-privileged, secret-free, observable, and verified at promotion. These practices do not replace application security, but they narrow the runtime boundary and make releases easier to explain when a vulnerability or rollback occurs.

Review the image at the edge

Inspect the image as the platform will run it, not only as a local developer. Confirm the final user, working directory, exposed port, writable paths, certificate store, process list, and signal handling. Verify that an error page does not dump environment values and that a temporary file cannot grow without a bound. These checks catch runtime assumptions that a static Dockerfile review misses.

Keep a release record with the digest, scan policy, exception owner, and rollback digest. Rebuild when the base image receives a security update, but promote the new digest only after the same startup and smoke tests pass. Hardening is a repeatable release property, not a one-time cleanup of a container file.

Review the image when the service gains a parser, shell command, native extension, or file-processing path. New runtime capabilities can change the threat model even if the image grew by only a few megabytes. Keep the principle simple: every binary and writable directory should have a reason, an owner, and a test that proves it is needed.

Reduce runtime capability as well as size

A small image can still be dangerous if the process has unnecessary Linux capabilities, writable system paths, or a shell it never needs. Run as a non-root user, use a read-only filesystem where possible, drop capabilities, and set resource limits at the platform boundary. Exercise the health endpoint, signal handling, certificate validation, and temporary-file path in the final image. Hardening is strongest when it is tested as the service actually runs.

Implementation example

Use a pinned base image, multi-stage build, non-root runtime user, minimal packages, read-only filesystem where possible, dropped capabilities, and bounded temporary storage. Scan the final image rather than only the builder stage and record the digest that passed. The runtime command must bind the platform port and handle termination signals.

dockerfile
FROM node:20-alpine AS runtime
USER 1001
ENV NODE_ENV=production
CMD ["node", "server.js"]

Verify and troubleshoot

Inspect the final image user, filesystem, exposed ports, certificate store, process list, writable paths, signal handling, and error responses. Test startup, health checks, graceful shutdown, temporary-file limits, dependency timeouts, and an image with a revoked or vulnerable package. Confirm scans and provenance gates run before deployment.

Operations and recovery

Rebuild when the base image or critical dependency changes, but promote only after the same smoke tests pass. Keep exception owner, reason, expiry, current digest, and rollback digest in the release record. If an image is compromised, stop promotion, revoke credentials exposed to its build or runtime, and roll back to a verified digest.

References and further reading

Use Docker image best practices, CIS container benchmarks, OWASP Container Security guidance, and the Cloud Run runtime contract. Every binary, capability, and writable path should have a reason and owner.

Keep exploring