DEV Community

Rishav Raj
Rishav Raj

Posted on

Dockerfile: From Basic to Production, Step by Step

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"]

Enter fullscreen mode Exit fullscreen mode

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"]

Enter fullscreen mode Exit fullscreen mode

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"]

Enter fullscreen mode Exit fullscreen mode

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"]

Enter fullscreen mode Exit fullscreen mode

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"]

Enter fullscreen mode Exit fullscreen mode

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)