Docker solves two specific problems for application developers: the "works on my machine" problem (your app works locally but fails on CI or in production because of environment differences), and the multi-service local development problem (your app needs a database, a Redis instance, and a mail server running locally). For these two use cases, Docker is the right tool. For everything else, it may be overkill.
Core Concepts Without the Jargon
Image vs. container. A Docker image is a snapshot of a filesystem and configuration — think of it as a recipe. A container is a running instance of an image — the actual running process. One image can spawn many containers. When you run docker run postgres, Docker pulls the Postgres image (if not already downloaded) and starts a container from it.
Dockerfile. A text file that describes how to build your image. It starts from a base image, copies your code in, installs dependencies, and defines how to start your app. Docker reads this file and produces an image.
docker-compose. A tool for running multiple containers together. Your web app container, database container, and Redis container are defined in one docker-compose.yml file. docker compose up starts all of them.
A Practical Dockerfile for a Node.js App
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
# Production stage
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
This is a multi-stage build. The builder stage installs all dependencies and compiles the app. The runner stage copies only the compiled output and production dependencies — discarding the build tools and dev dependencies. The result is a much smaller production image (often 200-400 MB instead of 800+ MB).
Key principles in this Dockerfile:
node:20-alpineuses Alpine Linux as the base, which is minimal and small- Copy
package.jsonand the lockfile before copying source code — this lets Docker cache thepnpm installlayer and skip it when only source code changes - The
--frozen-lockfileflag ensures the exact dependency versions in your lockfile are installed, giving you reproducibility
docker-compose for Multi-Service Local Development
version: "3.9"
services:
web:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=mongodb://mongo:27017/myapp
- REDIS_URL=redis://redis:6379
depends_on:
- mongo
- redis
volumes:
- .:/app
- /app/node_modules
mongo:
image: mongo:7
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
mongo-data:
docker compose up # Start all services
docker compose up -d # Start in background (detached)
docker compose down # Stop all services
docker compose logs -f web # Follow logs for the web service
The volumes: .:/app mount in the web service maps your local source code into the container. Combined with a Node.js hot-reload setup (nodemon or Next.js dev mode), changes to local files immediately reflect in the running container — no rebuild required.
The Practical Development Workflow
For local development with Docker Compose:
- Write your code locally in your editor (VS Code, Cursor, whatever)
docker compose upstarts your app and all dependencies- Edit files — hot reload keeps the running container current
- When you add a dependency, rebuild with
docker compose up --build docker compose downwhen done
This gives every developer on your team the same running environment regardless of their OS. A new developer can be running your app in 5 minutes with git clone and docker compose up, without installing Node, MongoDB, or Redis.
Multi-Stage Builds in Practice
Multi-stage builds keep your production images small and secure. The pattern:
# Stage 1: Install and build
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm build
# Stage 2: Minimal production image
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
The final runner image contains no build tools, no dev dependencies, no source code — only the compiled output and the runtime dependencies it needs. This reduces the attack surface and the image size.
When Docker Is Overkill for Local Dev
Docker for local development adds overhead: build times, memory usage, and configuration complexity. For a solo developer working on a Next.js app with only a MongoDB dependency, running MongoDB locally via Homebrew (brew services start mongodb-community) is faster and simpler.
Docker for local dev earns its cost when:
- Your team has 3+ developers on different OSes
- Your app has 3+ service dependencies
- A new developer setup currently takes more than an hour
- Environment differences are causing bugs (prod issues you can't reproduce locally)
Docker Desktop vs Alternatives on macOS
Docker Desktop is the most straightforward way to run Docker on macOS. It is free for personal use and small companies (under 250 employees and $10M revenue). For larger companies, it requires a paid license starting at $21/month per user.
OrbStack is the practical alternative on macOS. It runs Docker containers and Kubernetes, is significantly faster to start than Docker Desktop (3-4 seconds vs 30+ seconds), uses less memory, and costs $8/month after a free trial. For developers who run Docker every day, OrbStack is the better macOS experience.
Colima is a free, open source alternative to Docker Desktop's VM layer. It runs the Docker engine via a lightweight Linux VM. Less polished than OrbStack but zero cost.
.dockerignore — Don't Forget This
Without a .dockerignore file, Docker copies everything including node_modules, .git, .env files, and build artifacts into the image context. This makes builds slow and images large.
node_modules
.next
.git
.env
.env.local
*.log
coverage
.DS_Store
Keep Reading
- Monorepo with Turborepo Guide — Containerizing a multi-package monorepo
- Secrets Management Guide for Developers — How to handle
.envand secrets in Docker correctly - CI/CD for Small Engineering Teams — Building and pushing Docker images in GitHub Actions
Pristren builds AI-powered software for teams. Zlyqor is our all-in-one workspace — chat, projects, time tracking, AI meeting summaries, and invoicing — in one tool. Try it free.