Docker for Web Developers: A Complete Beginner's Tutorial
Learn Docker from scratch with practical examples for web development. Master containers, images, Docker Compose, and deployment workflows step by step.
Docker transformed how I deploy applications. No more "it works on my machine" problems. In this tutorial, I'll walk you through Docker fundamentals with practical web development examples that you can apply immediately.
Why Docker Matters for Web Developers
Before Docker, setting up a development environment meant:
- Installing specific Node.js versions
- Configuring databases locally
- Matching production environment settings
- Debugging environment-specific bugs
Docker solves all of this by packaging your application with its entire environment.
Core Concepts
Images vs Containers
Think of it like this:
- Image: A recipe (instructions to build your environment)
- Container: A dish made from that recipe (running instance)
# Pull an image from Docker Hub
docker pull node:20-alpine
# Create and run a container from that image
docker run -it node:20-alpine node --version
# Output: v20.x.x
Your First Dockerfile
Let's containerize a Next.js application:
# Dockerfile
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
WORKDIR /app
# Copy package files
COPY package.json package-lock.json* ./
RUN npm ci
# Build the application
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED 1
RUN npm run build
# Production image
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
# Create non-root user for security
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
CMD ["node", "server.js"]
Build and run:
# Build the image
docker build -t my-nextjs-app .
# Run the container
docker run -p 3000:3000 my-nextjs-app
# Visit http://localhost:3000
Essential Docker Commands
Managing Containers
# List running containers
docker ps
# List all containers (including stopped)
docker ps -a
# Stop a container
docker stop <container_id>
# Remove a container
docker rm <container_id>
# View container logs
docker logs <container_id>
# Follow logs in real-time
docker logs -f <container_id>
# Execute command in running container
docker exec -it <container_id> sh
Managing Images
# List local images
docker images
# Remove an image
docker rmi <image_id>
# Remove unused images
docker image prune
# Remove all unused Docker resources
docker system prune -a
Docker Compose for Development
Real applications need multiple services. Docker Compose orchestrates them:
# docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
environment:
- DATABASE_URL=postgresql://postgres:password@db:5432/myapp
- REDIS_URL=redis://cache:6379
depends_on:
- db
- cache
db:
image: postgres:16-alpine
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: myapp
volumes:
- postgres_data:/var/lib/postgresql/data
cache:
image: redis:7-alpine
ports:
- "6379:6379"
adminer:
image: adminer
ports:
- "8080:8080"
volumes:
postgres_data:
Development Dockerfile
# Dockerfile.dev
FROM node:20-alpine
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm install
# Copy source code
COPY . .
# Start development server with hot reload
CMD ["npm", "run", "dev"]
Compose Commands
# Start all services
docker-compose up
# Start in background
docker-compose up -d
# View logs
docker-compose logs -f app
# Stop all services
docker-compose down
# Rebuild images
docker-compose up --build
# Remove volumes (careful - deletes data!)
docker-compose down -v
Environment Variables and Secrets
Never hardcode secrets in Dockerfiles:
# docker-compose.yml
services:
app:
env_file:
- .env.local
environment:
- NODE_ENV=development
For production, use Docker secrets or external secret management:
# docker-compose.prod.yml
services:
app:
secrets:
- db_password
environment:
- DB_PASSWORD_FILE=/run/secrets/db_password
secrets:
db_password:
external: true
Optimizing Docker Images
Multi-stage Builds
Reduce image size by separating build and runtime:
# Build stage - includes dev dependencies
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage - only runtime dependencies
FROM node:20-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]
Layer Caching
Order Dockerfile commands for optimal caching:
# Good: Dependencies cached if package.json unchanged
COPY package*.json ./
RUN npm ci
COPY . .
# Bad: Dependencies reinstalled on every code change
COPY . .
RUN npm ci
.dockerignore
Exclude unnecessary files:
# .dockerignore
node_modules
.git
.gitignore
*.md
.env*
.next
dist
coverage
.DS_Store
Networking Between Containers
Containers in the same Compose network can reach each other by service name:
// app connects to database using service name
const prisma = new PrismaClient({
datasources: {
db: {
url: 'postgresql://postgres:password@db:5432/myapp'
// ^^-- service name, not localhost
}
}
});
// Redis connection
import Redis from 'ioredis';
const redis = new Redis({
host: 'cache', // service name from docker-compose
port: 6379
});
Health Checks
Ensure containers are actually ready:
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
# docker-compose.yml
services:
app:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
Debugging Docker Containers
Interactive Shell
# Enter a running container
docker exec -it myapp sh
# Run commands inside
ls -la
cat package.json
node --version
Inspect Container Details
# Full container info
docker inspect <container_id>
# Get specific value
docker inspect --format='{{.NetworkSettings.IPAddress}}' <container_id>
Common Issues
Container exits immediately:
# Check logs
docker logs <container_id>
# Keep container running for debugging
docker run -it myapp sh
Cannot connect to service:
# Verify ports are mapped
docker ps
# Check container networking
docker network ls
docker network inspect <network_name>
Production Deployment Tips
- Use specific image tags:
node:20.10-alpinenotnode:latest - Run as non-root user: Security best practice
- Scan for vulnerabilities:
docker scan myimage - Limit resources: Prevent runaway containers
services:
app:
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
What's Next
Docker is the foundation for Kubernetes, CI/CD pipelines, and modern deployment workflows. Master these basics, and you'll be ready for:
- Container orchestration with Kubernetes
- CI/CD with GitHub Actions and Docker
- Cloud deployments on AWS ECS, Google Cloud Run, or Azure Container Apps
Check out my Vercel deployment guide for simpler deployment options, or explore Node.js best practices to write container-ready code.