Day 37: Image optimization: distroless, layer caching
Making images small and safe
A smaller image pulls faster, scans faster, and — critically — has a smaller attack surface. Distroless base images ship only your application and its runtime dependencies, deliberately omitting a shell, package manager, or any other tool an attacker could use after a compromise.
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["dist/index.js"]This connects straight to Phase 26 (OWASP) and Phase 11 (K8s Security)
If an attacker exploits your app, distroless means there's no shell for them to pivot with — kubectl exec into a distroless container won't give you a shell either, which is a feature, not a limitation.
Layer caching strategy
- Order instructions from least-frequently-changing to most-frequently-changing
- Combine related RUN commands to avoid unnecessary intermediate layers
- Use a .dockerignore to keep node_modules/.git out of the build context entirely
Key terms
- Distroless image
- A minimal base image with only the app and its runtime deps — no shell, package manager, or extra tools.
- .dockerignore
- Excludes files from the build context, keeping builds fast and images free of unwanted files.
Phase 5 capstone: containerize the full stack
Containerize a NestJS API, a Next.js frontend, PostgreSQL, Redis, and RabbitMQ — one Dockerfile per app service (multi-stage, distroless or -slim final stage), one docker-compose.yml wiring them together. Target: your API image under 150MB. Check with docker images and iterate on layer ordering and base image choice until you hit it.
Why can't you docker exec into a shell inside a distroless container, and why is that good?