Skip to content

Dockerfiles

A Dockerfile is a recipe for building a container image — every instruction creates a layer and layers stack like pancakes so ordering matters and every line you add bloats the final artifact if you don't know what you're doing

Dockerfile Instructions — The Full Arsenal

Each instruction creates a layer in the final image. More layers = bigger image = slower pulls

FROM — Your Base Image

Always start with something minimal. Alpine Linux is tiny (5MB) but uses musl libc which breaks some Node native modules. Debian slim is a safer bet for Node apps

# Bad — 1.2GB image
FROM node:20

# Better — 175MB
FROM node:20-slim

# Smallest — 125MB but potential C++ addon issues
FROM node:20-alpine

WORKDIR — Set Your Working Directory

Sets the working directory for RUN , CMD , ENTRYPOINT , COPY , ADD Creates the directory if it doesn't exist

WORKDIR /app
# All subsequent commands run from /app

COPY — Bring Your Code In

# Copy package.json first for layer caching
COPY package.json package-lock.json ./

# Then copy the rest of the app
COPY . .

Order matters — Docker caches layers. If package.json hasn't changed , Docker reuses the cached npm install layer instead of reinstalling every time

RUN — Execute Commands During Build

RUN npm install --production

# Chain commands to reduce layers
RUN apt-get update && \
    apt-get install -y curl && \
    rm -rf /var/lib/apt/lists/*

EXPOSE — Documentation (doesn't actually publish ports)

# Just metadata — you still need -p when running
EXPOSE 3000

CMD vs ENTRYPOINT — The Confusion Ends Here

# CMD — default command , can be overridden
CMD ["node", "server.js"]

# ENTRYPOINT — always runs , arguments get appended
ENTRYPOINT ["node"]
CMD ["server.js"]  # Default argument to ENTRYPOINT

Difference: docker run myimage worker.js with CMD overrides the whole thing With ENTRYPOINT+CMD , it runs node worker.js

Production Dockerfile for Node.js — Stop Copying From Tutorials

# ---- Build Stage ----
FROM node:20-slim AS builder

WORKDIR /app

# Copy only dependency files first (layer caching optimization)
COPY package.json package-lock.json ./
RUN npm ci --only=production

# Copy source code
COPY . .

# ---- Production Stage ----
FROM node:20-slim

WORKDIR /app

# Create non-root user
RUN groupadd -r appgroup && useradd -r -g appgroup -m appuser

# Copy only production dependencies and built app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app .

EXPOSE 3000

# Switch to non-root user
USER appuser

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node healthcheck.js || exit 1

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

.dockerignore — Don't Ship Your Node Modules Twice

node_modules
npm-debug.log
.git
.gitignore
.env
.env.*
*.md
Dockerfile
.dockerignore
.gitkeep
coverage
.nyc_output
dist
.eslintcache

This prevents accidentally copying node_modules from your host (which are for your host's architecture) into the image

Multi-Stage Builds — The Real Power Move

Multiple FROM statements in one Dockerfile. Each FROM starts a new stage. Copy artifacts between stages. Final image only contains what you need

Why this matters: * Build stage has the full SDK , build tools , dev dependencies * Production stage is minimal — just runtime and compiled output * Your image goes from 1.2GB to 150MB

# Stage 1: Build
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Production
FROM node:20-slim
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package.json ./
EXPOSE 3000
CMD ["node", "dist/server.js"]

Optimizing Image Size — Every Megabyte Counts

Smaller images = faster deploys , less bandwidth , fewer CVEs to patch

Rule of thumb: * Use slim or alpine base images where possible * Combine RUN commands to reduce layers * Clean up package manager caches in the same RUN layer * Use multi-stage builds religiously * NEVER include dev dependencies in production image

# Bad — bloated
FROM node:20
COPY . .
RUN npm install
CMD ["node", "server.js"]

# Good — lean
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY . .
USER node
CMD ["node", "server.js"]

Common Mistakes That Will Haunt You

RUN apt-get update without cleanup:

# Bad — leaves apt cache in layer
RUN apt-get update
RUN apt-get install -y curl

# Good — clean up in same layer
RUN apt-get update && \
    apt-get install -y curl && \
    rm -rf /var/lib/apt/lists/*

Copying entire context without .dockerignore: Your build context includes node_modules from host , .git history , all your meme folders Use .dockerignore or structure your build to only send what's needed

Running as root: Every Docker tutorial runs as root and that's fine for local but in production your container gets root on the host if there's an escape Always create a non-root user


next → devops_03_docker_compose.md