Skip to main content
Back to Blog
DockerDevOpsWeb DevelopmentContainersNode.jsDeployment

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.

6 min read

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

  1. Use specific image tags: node:20.10-alpine not node:latest
  2. Run as non-root user: Security best practice
  3. Scan for vulnerabilities: docker scan myimage
  4. 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.