Dockerfile Evolution: From Basic Containers to Production-Ready Supply-Chain-Aware Builds
This article explains how a Dockerfile evolves step by step to meet real production, security, and portability requirements.
Layer 1 — Basic Dockerfile
Run the application inside a container with minimum effort and validate runtime behavior.
Uses a single-stage Dockerfile with an official base image and application code copied directly.
Installs dependencies and runs the application in the same image without build-time separation.
Focuses only on correctness, not image size, security posture, or long-term maintainability.
Suitable for learning, prototyping, or validating that the application runs inside containers.
Lacks optimization, reproducibility, and security hardening required for production workloads.
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]
Note for beginners: The * is a wildcard. It means all files starting with package and ending with .json will be copied.
Layer 2 — Optimized Base Image (Slim / Alpine)
Reduce image size and remove unnecessary operating system components.
This layer focuses on reducing image size by switching from a full OS image to Alpine or Slim variants.
The application runtime remains the same while unnecessary system packages are removed.
Smaller images lead to faster pull times and reduced storage usage.
Build speed and startup time improve without changing application logic.
This layer introduces optimization without increasing Dockerfile complexity.
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
Layer 3 — Multi-Stage Build
Use multi-stage builds to separate build and runtime concerns while applying basic production-grade Dockerfile practices.
This layer is useful when the application has a build or compilation step before runtime, such as TypeScript, Java, or Python packaging.
Multi-stage builds isolate compilers and build tools from the runtime image.
Runtime images include only build artifacts and required production dependencies.
Basic production practices like version pinning, non-root execution, and health checks are introduced.
This layer represents a stable mid-level production Dockerfile without advanced security or supply-chain tooling.
FROM node:18.19.0-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18.19.0-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
EXPOSE 3000
RUN addgroup -S app && adduser -S app -G app
USER app
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', res => process.exit(res.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"
CMD ["node", "dist/index.js"]
Layer 4 — Distroless + Reproducible Build
Create a secure, minimal, reproducible container with production hygiene, metadata, healthcheck, non-root execution, and pinned versions.
Uses distroless images to remove shells, package managers, and unnecessary OS utilities, keeping the runtime minimal and secure.
Base images pinned using SHA digests and dependencies locked via package-lock.json for fully reproducible builds across environments.
Includes LABEL metadata like maintainer name, email, version, and description for traceability and legacy maintenance support.
Creates a non-root user and adds a basic HEALTHCHECK to improve container safety and orchestrator readiness.
Copies only runtime artifacts; development tools, compilers, and unnecessary packages remain in the build stage, reducing attack surface.
Note: Distroless images are not always smaller in size.
Sometimes they look bigger because the full runtime and required dependencies are bundled.
The main purpose of distroless is better security, reproducibility, and a reduced attack surface, not just image size optimization.
For deeper understanding, refer to the official Docker documentation.
FROM node:18.19.0-alpine@sha256:0085670310d2879621f96a4216c893f92e2ded827e9e6ef8437672e1bd72f437 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM gcr.io/distroless/nodejs:18@sha256:b534f9b5528e69baa7e8caf7bcc1d93ecf59faa15d289221decf5889a2ed3877
WORKDIR /app
ARG APP_VERSION=1.0.0
LABEL maintainer="Anonymous <abc@example.com>" \
description="Production-ready Node.js app with distroless and reproducible build" \
version="$APP_VERSION"
COPY --from=builder /app/dist ./dist
EXPOSE 3000
USER nonroot
HEALTHCHECK CMD node -e "process.exit(0)"
CMD ["dist/index.js"]
Layer 5 — Production-Ready Multi-Arch Distroless Build
Build a secure, minimal, reproducible, and portable container for multiple architectures with proper metadata and hygiene.
Uses distroless images to remove shells, package managers, and unnecessary OS utilities, keeping the runtime minimal and secure.
Builds multi-architecture images (amd64 + arm64) ensuring portability across cloud, ARM servers, and heterogeneous infrastructures.
Base images pinned via SHA and dependencies locked through package-lock.json to guarantee reproducible builds every time.
Includes ARGs, LABEL metadata, non-root user, and basic HEALTHCHECK to improve maintainability, security, and orchestrator readiness.
Copies only runtime artifacts from the build stage; dev tools and compilers remain isolated, reducing attack surface.
FROM --platform=$BUILDPLATFORM node:18.19.0-alpine@sha256:0085670310d2879621f96a4216c893f92e2ded827e9e6ef8437672e1bd72f437 AS builder
WORKDIR /app
ARG NODE_ENV=production
ARG APP_VERSION=1.0.0
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM --platform=$TARGETPLATFORM gcr.io/distroless/nodejs:18@sha256:b534f9b5528e69baa7e8caf7bcc1d93ecf59faa15d289221decf5889a2ed3877
WORKDIR /app
ARG APP_VERSION=1.0.0
LABEL maintainer="Anonymous <anonymous@example.com>" \
description="Production-ready Node.js app with distroless, reproducible, multi-arch build" \
version="$APP_VERSION"
COPY --from=builder /app/dist ./dist
EXPOSE 3000
USER nonroot
HEALTHCHECK CMD node -e "process.exit(0)"
CMD ["dist/index.js"]
The screenshot below shows the final container image size after applying multi-stage builds, distroless base images, and reproducible build practices, highlighting the impact of each optimization layer.

Top comments (0)