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.
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 -3Expected 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 smokeExpected 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 -3Expected 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.