Docker for JavaScript Developers: A No-Nonsense Guide
Everything you actually need to know about Docker as a JS dev — from writing your first Dockerfile to production multi-stage builds that keep images small.
Why Bother With Docker?
"It works on my machine" is a cliché because it's a real problem. Docker solves it by packaging your app and everything it needs — Node version, OS libs, environment — into a portable image that runs identically everywhere.
For JS developers specifically, Docker gives you: consistent dev environments across a team, easy local service dependencies (Postgres, Redis), and reproducible production builds on any cloud.
Your First Dockerfile
Start with a basic Node.js app:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
A few choices explained: node:20-alpine uses the Alpine Linux base, which is ~5MB vs ~900MB for the full Debian image. npm ci instead of npm install is deterministic — it installs exactly what's in your lockfile.
The Multi-Stage Build (Production Pattern)
Single-stage builds include dev dependencies, build tools, and source files in the final image. Multi-stage builds fix this:
# ── Stage 1: Build ──────────────────────────
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ── Stage 2: Production ──────────────────────
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Only copy what we need
COPY --from=builder /app/package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]
The final image contains zero build tools and zero dev dependencies. For a typical Next.js app this takes you from ~1.5GB down to ~200MB.
Docker Compose for Local Dev
Running your app with its dependencies (Postgres, Redis) locally:
# docker-compose.yml
services:
app:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://postgres:password@db:5432/myapp
REDIS_URL: redis://cache:6379
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: password
POSTGRES_DB: myapp
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
retries: 5
volumes:
- postgres_data:/var/lib/postgresql/data
cache:
image: redis:7-alpine
volumes:
postgres_data:
docker compose up starts everything. docker compose down -v tears it all down including data. Your team clones the repo, runs one command, and has a working environment.
.dockerignore Is Not Optional
Without a .dockerignore, COPY . . sends your entire project including node_modules (often 500MB+) to the Docker build context:
node_modules
.next
dist
.git
*.log
.env*
This alone can cut build times from minutes to seconds on first run.
The Patterns That Matter
For JS developers, three things deliver 80% of the value: multi-stage builds for small images, Compose for local dev parity, and npm ci over npm install for reproducibility. Everything else — Kubernetes, swarm, custom networks — you can learn when you need it. Start with these three.
Related Articles
The Underrated Power of CSS Grid Subgrid
Subgrid finally has solid browser support. Here's how it solves alignment problems that used to require JavaScript hacks, and why you should be using it today.