← Course Index

Writing Dockerfiles

~30 min · Docker · Docker Cheat Sheet →

Ref
Primary Source
Docker Docs — Dockerfile Best Practices

Official guide with all Dockerfile instructions and production best practices. Essential reference. docs.docker.com →

Dockerfile Instructions

A Dockerfile is a recipe — each instruction creates a layer in the image.

InstructionWhat it does
FROMBase image to build on (always first)
WORKDIRSet working directory for all subsequent commands
COPYCopy files from host into the image
RUNExecute a shell command (creates a layer)
ENVSet an environment variable
EXPOSEDocument which port the container listens on (informational)
USERSet which user runs subsequent commands (security)
CMDDefault command when container starts (overridable)
ENTRYPOINTFixed startup command (not easily overridden)
ARGBuild-time variable (not in final image)

A Production-Ready Node.js Dockerfile

Here's a real Dockerfile with every best practice applied:

# ── Stage 1: Dependencies ──────────────────────────────────────
FROM node:20-alpine AS deps

WORKDIR /app

# Copy ONLY package files first — maximise cache hits
# This layer only rebuilds when dependencies change
COPY package.json package-lock.json ./
RUN npm ci --only=production

# ── Stage 2: Final image ────────────────────────────────────────
FROM node:20-alpine AS runner

WORKDIR /app

# Security: don't run as root
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodeuser -u 1001

# Copy deps from build stage
COPY --from=deps --chown=nodeuser:nodejs /app/node_modules ./node_modules

# Copy application code
COPY --chown=nodeuser:nodejs . .

# Set environment
ENV NODE_ENV=production
ENV PORT=3000

# Switch to non-root user
USER nodeuser

# Document port (doesn't actually expose it)
EXPOSE 3000

# Health check — Docker and orchestrators use this
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
  CMD wget -qO- http://localhost:3000/health || exit 1

# Start the app
CMD ["node", "server.js"]
💡 Why multi-stage?

The first stage (deps) installs dependencies. The second stage (runner) copies only what's needed into a clean image. This keeps the final image small and excludes build tools that shouldn't be in production.

The .dockerignore File

Like .gitignore but for Docker. Without it, COPY . . copies node_modules, .git, and all your secrets into the image. Create .dockerignore in your project root:

node_modules
.git
.gitignore
*.md
.env
.env.*
dist
.DS_Store
coverage
*.log
.nyc_output
🚨 Never copy .env into an image

If your image is pushed to a registry with a .env file inside it, your secrets are public. Always include .env in .dockerignore. Pass secrets as environment variables at runtime, never at build time.

Choosing Your Base Image

Base imageSizeUse when
node:20~1GBNever in production — too fat
node:20-slim~240MBGood general default
node:20-alpine~50MBBest for production — musl libc, minimal attack surface
cgr.dev/chainguard/node~30MBMaximum security — no shell, distroless

Building & Testing

# Build image
docker build -t myapp:dev .

# Run it
docker run -p 3000:3000 --env-file .env myapp:dev

# Pass a single env var
docker run -p 3000:3000 -e NODE_ENV=production myapp:dev

# Test your health endpoint
curl http://localhost:3000/health

# Inspect the image layers
docker history myapp:dev

# See the final image size
docker images myapp

Check Your Understanding

1. Why do we COPY package.json and run npm install BEFORE copying the rest of the source code?
2. Which of these should you NEVER include in your .dockerignore?
3. What does the USER instruction in a Dockerfile do?
4. For a production Node.js API, which base image is the best choice?