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