Docker for Frontend Developers
A practical introduction to Docker from a frontend perspective — containerizing Next.js apps, managing development environments, and CI/CD.
Why Frontend Developers Need Docker
Docker isn't just for backend engineers. As frontend applications grow in complexity — with environment variables, API proxies, SSR, and serverless functions — Docker provides consistent environments from development through production.
The Problem
How many times have you heard "it works on my machine"? Different Node versions, operating systems, and system dependencies cause subtle bugs that waste hours.
Docker solves this by packaging your entire environment into a container.
Your First Dockerfile
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
# Build the application
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN bun run build
# Production image
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]
Multi-Stage Builds
The Dockerfile above uses multi-stage builds — a pattern that keeps the final image small by discarding build tools and intermediate artifacts:
| Stage | Purpose | Size |
|---|---|---|
base | Base Node image with system deps | ~150MB |
deps | Install dependencies | Adds ~200MB (discarded later) |
builder | Compile the app | Adds ~50MB (discarded later) |
runner | Production image — only what's needed | ~180MB final |
Docker Compose for Development
# docker-compose.yml
services:
app:
build:
context: .
target: deps
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
environment:
- NODE_ENV=development
- CONVEX_URL=${CONVEX_URL}
command: bun run dev
db:
image: postgres:16-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: myapp
POSTGRES_PASSWORD: secret
volumes:
postgres_data:
Essential Docker Commands
# Build the image
docker build -t my-app .
# Run the container
docker run -p 3000:3000 my-app
# Development with compose
docker compose up
# List running containers
docker ps
# View logs
docker logs -f my-app
# Clean up
docker system prune -a
Optimizing for Next.js
Standalone Output
Next.js can produce a standalone output that includes only the files needed for production:
// next.config.js
module.exports = {
output: "standalone",
}
This reduces the deployment package significantly and works perfectly with the multi-stage Dockerfile pattern above.
Environment Variables
# Build-time args
ARG NEXT_PUBLIC_SITE_URL
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
# Runtime env vars (can be overridden at deployment)
ENV DATABASE_URL=""
ENV AUTH_SECRET=""
CI/CD Integration
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t my-app .
- name: Deploy to server
run: |
docker save my-app | ssh user@server docker load
ssh user@server "docker compose up -d"
Best Practices
- Use
.dockerignore— excludenode_modules,.next,.git - Pin base image versions — avoid
:latest, use:20-alpine - Layer caching — order instructions from least to most frequently changing
- Health checks — add
HEALTHCHECKto detect failing containers - Non-root user — create and use a non-root user for security
Conclusion
Docker eliminates environment inconsistencies and streamlines deployment. Start with a simple Dockerfile for your Next.js app, add Docker Compose for development, and expand from there. The upfront investment pays for itself in the first debugging session saved.