← Recipes

阅读中文版本

Migrate a Runtime Image to Distroless Safely

Shrink the attack surface by moving from a full base image to distroless, keeping debug shells reachable via a separate debug tag, and verify in CI.

~30 mindeploydockersecurity

Task

Move an application image to a distroless base while preserving debuggability and proving the runtime still passes smoke tests.

When to use this

The production image carries an entire Debian with apt, curl, and a root shell. The security audit lists hundreds of CVEs in packages the app never uses.

Prerequisites

  • A statically analyzable application, or a runtime variant matching your stack
  • A smoke test suite runnable against a container
  • A registry able to hold two tags per version

Steps

1. Baseline the current image size and CVE count.

docker images app:current --format "{{.Size}}" && trivy image --severity HIGH,CRITICAL app:current | tail -3

Expected output: A starting point such as 950 MB with 212 HIGH findings quantifies what the migration should improve.

2. Switch the base to the matching distroless variant.

FROM node:22-slim AS build
WORKDIR /app
COPY . .
RUN npm ci --omit=dev

FROM gcr.io/distroless/nodejs22
WORKDIR /app
COPY --from=build /app /app
USER nonroot
CMD ["server.js"]

Expected output: The final stage contains the runtime, application code, and nothing else; no shell, no package manager, no root.

3. Keep a debug tag with a shell for incidents.

FROM gcr.io/distroless/nodejs22:debug
WORKDIR /app
COPY --from=build /app /app
USER nonroot
CMD ["server.js"]

Expected output: The debug variant includes busybox, so a stuck container can still be exec-ed into during an incident.

4. Run the smoke suite against the new image.

docker build -t app:distroless . && docker run --rm -d -p 3000:3000 app:distroless && npm run smoke

Expected output: The smoke tests pass, proving the runtime works without the removed OS packages.

5. Re-measure and gate the improvement in CI.

docker images app:distroless --format "{{.Size}}" && trivy image --severity HIGH,CRITICAL app:distroless | tail -3

Expected output: Size drops to around 120 MB and HIGH findings fall to single digits; CI gates keep both from regressing.

Troubleshooting

Problem

Container crashes immediately with a cryptic error

Solution

The app shells out to system tools that no longer exist; find the subprocess call in logs and replace it with a library, or keep the slim base for that service.

Problem

Cannot debug a running container

Solution

That is the point of distroless; use the separate debug tag in incident tooling, or kubectl debug with an ephemeral container.

Problem

Health check uses wget or curl

Solution

The probe binary is gone; switch the health check to a language-level endpoint check or TCP probe instead.

Limitations

  • Distroless removes the shell that many debugging habits depend on; the debug tag must be part of the workflow, not an afterthought.
  • Native extensions may require specific libraries; pick the distroless variant matching your runtime or stay on slim.

Delete the operating system you do not use

A typical application image ships a full Linux distribution to run one process. Every package manager, shell, and utility in that base is attack surface and CVE noise the application never needed. Distroless images contain the runtime and the application, full stop. The migration is usually a two-line Dockerfile change; the real work is preserving your ability to debug what you can no longer shell into.

FROM gcr.io/distroless/nodejs22
WORKDIR /app
COPY --from=build /app /app
USER nonroot
CMD ["server.js"]

# Incidents still need a shell: build a second tag
FROM gcr.io/distroless/nodejs22:debug

The debug tag is the exit

Removing the shell removes your привычный escape hatch, so provide a new one deliberately. Build a debug variant of the same application from the distroless debug base, publish it alongside every release, and document it in the runbook. The incident you cannot exec into is the incident that turns into an outage.

Let CI defend the win

The size and CVE improvements are only real if they stay. Add two gates to the pipeline: image size under a budget, and zero HIGH or CRITICAL findings in the scan. Both regress the moment someone adds a convenience package back to the image, which is exactly the drift the gates exist to catch.