Home/Learn/Docker/Writing Production-Grade Dockerfiles

Writing Production-Grade Dockerfiles

Beginner
Fundamentals

Production Dockerfiles use multi-stage builds, non-root users, .dockerignore, explicit version pinning, and health checks to produce small, secure, reproducible images.

Overview

A naive Dockerfile that "works" can produce 1 GB images containing build tools, dev dependencies, and running as root — a security and performance nightmare. Multi-stage builds are the single most impactful technique: use a fat build stage to compile your app, then copy only the final artifact into a minimal runtime image. Combined with a .dockerignore file, non-root user, pinned base image versions, and a HEALTHCHECK, your production Dockerfile becomes small, secure, and deterministic.

Multi-Stage Builds

Multi-stage builds use multiple FROM instructions in one Dockerfile. Only the final stage becomes the image. Earlier stages are used as build environments and are discarded, along with their tools and intermediate files.

Dockerfile — multi-stage Node.js build
# ──── Stage 1: build ────────────────────────────────

FROM node:20-alpine AS builder

WORKDIR /app

COPY package*.json ./

RUN npm ci                        # includes devDependencies

COPY . .

RUN npm run build                 # compile TypeScript, bundle, etc.



# ──── Stage 2: production runtime ────────────────────

FROM node:20-alpine AS runtime    # fresh image — no build tools

WORKDIR /app



# Only copy what the app needs to run

COPY --from=builder /app/dist ./dist

COPY --from=builder /app/node_modules ./node_modules

COPY package.json .



# Security: run as non-root

RUN addgroup -S appgroup && adduser -S appuser -G appgroup

USER appuser



EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \

  CMD wget -qO- http://localhost:3000/health || exit 1



CMD ["node", "dist/server.js"]



# Result:

# Without multi-stage: ~700 MB (includes TypeScript compiler, source maps, devDeps)

# With multi-stage:    ~180 MB (only runtime + dist output)

.dockerignore — Exclude What You Do Not Need

Without .dockerignore, the Docker build context sends your entire project directory to the daemon — including node_modules (300 MB+), .git, secrets, and local env files. This slows every build and can leak sensitive data into the image.

.dockerignore — exclude node_modules and secrets
# .dockerignore (place next to Dockerfile)

node_modules/        # re-installed inside container; don't copy host's

.git/                # git history not needed in image

.env                 # never bake secrets into images!

.env.local

dist/                # rebuilt inside container

coverage/

*.log

.DS_Store

README.md

docker-compose*.yml  # runtime config, not build artifact

.github/



# Verify your build context size:

# docker build --no-cache . 2>&1 | grep "Sending build context"

# Before .dockerignore: Sending build context to Docker daemon 480.1MB

# After  .dockerignore: Sending build context to Docker daemon  12.5MB

Security Hardening

Containers run as root by default. A vulnerability in your app can give attackers root access to your container and potentially the host. Always create and switch to a non-root user. Also pin base image versions for reproducibility.

Dockerfile — non-root user and version pinning
FROM node:20.15.1-alpine3.20   # ← pin exact version, not 'node:latest'



# Create non-root user and group

RUN addgroup -S appgroup && \

    adduser  -S appuser -G appgroup



WORKDIR /app

COPY --chown=appuser:appgroup . .   # set ownership at copy time

RUN npm ci --only=production



USER appuser                        # switch before CMD



# Verify non-root at runtime:

# docker run my-app whoami  →  appuser



# Also: use 'distroless' for even smaller, no-shell images

# FROM gcr.io/distroless/nodejs20-debian12 AS runtime

# No bash, no apt, no package manager — minimal attack surface

Key Points to Remember

  • 1Multi-stage builds separate build environment from runtime — dramatically smaller images.
  • 2.dockerignore prevents leaking node_modules, .git, and .env files into the build context.
  • 3Never run containers as root in production — always add a non-root user.
  • 4Pin exact base image versions (node:20.15.1-alpine3.20) for reproducibility.
  • 5HEALTHCHECK lets Docker and orchestrators know if the container is actually healthy.
  • 6Never bake secrets (API keys, passwords) into Docker images — use env vars or secrets managers.

Interview Questions

Sign in to ask Aria
1

What is a multi-stage Docker build and why do you use it?

2

Why should you not run containers as root?

3

What goes in .dockerignore and why?

Ask Aria about Writing Production-Grade Dockerfiles

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…