Every developer eventually hits the "works on my machine" wall. Docker is the permanent solution. This guide skips the theory and gets you to working containers fast.
Install Docker
Download Docker Desktop for Mac or Windows. On Linux:
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
Verify:
docker --version # Docker version 25.x.x
docker compose version # Docker Compose version v2.x.x
Your First Dockerfile
For a Node.js/Next.js app:
FROM node:22-alpine
WORKDIR /app
# Copy dependency files first (layer caching)
COPY package*.json ./
RUN npm ci
# Copy source
COPY . .
# Build
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
Build and run:
docker build -t my-app .
docker run -p 3000:3000 my-app
Visit http://localhost:3000. That's it.
Multi-Stage Builds (The Right Way)
Single-stage builds ship your node_modules and source into production. Multi-stage builds keep your image lean:
# ── Stage 1: Dependencies ─────────────────────────────
FROM node:22-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# ── Stage 2: Builder ──────────────────────────────────
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ── Stage 3: Runner ───────────────────────────────────
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Only copy what's needed at runtime
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./
EXPOSE 3000
CMD ["npm", "start"]
Result: 180MB image instead of 1.2GB. Smaller image = faster pulls, faster cold starts.
[!TIP] Always add a
.dockerignorefile. It works like.gitignoreand prevents node_modules, .git, and local env files from being copied into the build context.
.dockerignore
node_modules
.next
.git
.env*.local
*.md
Docker Compose for Local Development
Running a full stack locally — app, database, Redis — without Docker Compose means managing multiple terminal windows. With it:
# docker-compose.yml
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:password@db:5432/myapp
- REDIS_URL=redis://cache:6379
volumes:
- .:/app
- /app/node_modules # Don't override node_modules with host
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
postgres_data:
Start everything:
docker compose up -d # Start in background
docker compose logs -f # Tail logs
docker compose down # Stop everything
docker compose down -v # Stop + delete volumes (fresh state)
Environment Variables
Never hardcode secrets. Use .env files with Docker Compose:
services:
app:
env_file:
- .env.local
# .env.local
DATABASE_URL=postgresql://...
REDIS_URL=redis://...
RESEND_API_KEY=re_...
For production, use your platform's secret management (Railway secrets, Vercel env, AWS Secrets Manager).
Useful Commands
# List running containers
docker ps
# Execute a command inside a running container
docker exec -it <container_name> sh
# Check container logs
docker logs <container_name> -f
# Remove all stopped containers + unused images
docker system prune
# Remove everything including volumes (destructive!)
docker system prune --volumes
# Inspect a container's filesystem
docker exec -it <container_name> ls /app
# Copy files out of a container
docker cp <container_name>:/app/output.json ./output.json
Production Checklist
Before deploying a Dockerised app:
[ ]Multi-stage build (minimal image size)[ ].dockerignoreconfigured[ ]Running as non-root user:USER nodein Dockerfile[ ]Health check endpoint:HEALTHCHECK CMD curl -f http://localhost:3000/api/health[ ]Secrets via environment, not baked into image[ ]Image tagged with git SHA, not justlatest[ ]Resource limits set in your orchestration config
Next Steps
This gets you to a working Docker setup. For production at scale, the next layers to explore:
- Kubernetes — orchestrating many containers across many machines
- Docker Hub / GHCR — storing and versioning your images
- CI/CD integration — building and pushing images automatically on merge
But for most apps — especially early-stage products — Docker + Docker Compose + a managed platform (Railway, Fly.io, Render) is all you need.
Tagged with
Written by
DebuggerMe TeamThe DebuggerMe team builds developer tools, writes technical content, and helps teams ship better software.
Related Articles
All articles →Node.js 22 Is Here — Everything You Need to Know
Node.js 22 lands as the new LTS with native TypeScript type-stripping, a built-in test runner improvements, WebSocket client, and the much-anticipated require(esm) support. Here's a practical breakdown.
package.json vs package-lock.json – What's the Difference?
Understanding why both package.json and package-lock.json exist in your Node.js project, and when each file matters.
5 Git Workflows That Will Make Your Team More Productive
Most teams use Git without a consistent workflow. The result: messy history, painful merges, and deploy anxiety. These 5 workflows — from simple to advanced — will clean up your process.