Docker is an open-source containerization platform that packages an application together with its dependencies, runtime, libraries, and configuration, into a portable unit called a container, which runs identically on any machine with a container runtime. Unlike virtual machines, containers share the host operating system kernel, so they start in seconds and use megabytes rather than gigabytes of overhead. An image is the immutable blueprint built from a Dockerfile; a container is a running instance of that image; and Docker Compose orchestrates multi-container applications from a single YAML file. Containerization eliminates the classic works-on-my-machine problem by making development, testing, and production environments consistent. This guide covers Docker from the ground up: core concepts, writing efficient Dockerfiles with multi-stage builds, Compose for local stacks, networking, volumes, debugging, best practices, and strategies for deploying containers to production.
Why Docker?
Problems Docker solves:
- "It works on my machine" syndrome - eliminated
- Environment inconsistencies between dev/staging/prod
- Complex dependency management
- Slow onboarding for new team members
- Resource-heavy virtual machines
Core Concepts
Images vs Containers
An image is a blueprint (like a class), while a container is a running instance (like an object). Images are immutable and layered; containers are ephemeral and can be started, stopped, and destroyed.
# Pull an image from Docker Hub
docker pull node:20-alpine
# List all images
docker images
# Run a container from an image
docker run -d --name my-app -p 3000:3000 node:20-alpine
# List running containers
docker ps
# List all containers (including stopped)
docker ps -a
# Stop a container
docker stop my-app
# Remove a container
docker rm my-appHow Do You Write a Good Dockerfile?
Basic Node.js Dockerfile
# Use official Node.js image as base
FROM node:20-alpine
# Set working directory
WORKDIR /app
# Copy package files first (better caching)
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy application code
COPY . .
# Expose port
EXPOSE 3000
# Define the command to run
CMD ["node", "server.js"]Multi-Stage Builds (Production Ready)
Multi-stage builds let you use multiple FROM statements, keeping your final image small by only including what's needed for production.
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:20-alpine AS production
WORKDIR /app
# Copy only production dependencies
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
# Copy built assets from builder stage
COPY --from=builder /app/dist ./dist
# Create non-root user for security
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
USER nodejs
EXPOSE 3000
CMD ["node", "dist/server.js"]React/Vite Production Dockerfile
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage with Nginx
FROM nginx:alpine AS production
# Copy custom nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copy built assets
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]Docker Compose
Docker Compose lets you define and run multi-container applications. Perfect for development environments with databases, caches, and multiple services.
Full Stack Development Setup
# docker-compose.yml
version: '3.8'
services:
# Frontend
frontend:
build:
context: ./frontend
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
- ./frontend:/app
- /app/node_modules
environment:
- VITE_API_URL=http://localhost:4000
depends_on:
- backend
# Backend API
backend:
build:
context: ./backend
dockerfile: Dockerfile.dev
ports:
- "4000:4000"
volumes:
- ./backend:/app
- /app/node_modules
environment:
- DATABASE_URL=postgres://user:password@db:5432/myapp
- REDIS_URL=redis://cache:6379
depends_on:
- db
- cache
# PostgreSQL Database
db:
image: postgres:16-alpine
ports:
- "5432:5432"
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
- POSTGRES_DB=myapp
volumes:
- postgres_data:/var/lib/postgresql/data
# Redis Cache
cache:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:Docker Compose Commands
# Start all services
docker-compose up -d
# Start and rebuild images
docker-compose up -d --build
# View logs
docker-compose logs -f backend
# Stop all services
docker-compose down
# Stop and remove volumes (clean slate)
docker-compose down -v
# Run a command in a service
docker-compose exec backend npm run migrate
# Scale a service
docker-compose up -d --scale backend=3Networking
Docker provides several network modes. In Docker Compose, services can communicate using service names as hostnames.
# Custom network configuration
version: '3.8'
services:
frontend:
networks:
- frontend-network
backend:
networks:
- frontend-network
- backend-network
db:
networks:
- backend-network
networks:
frontend-network:
driver: bridge
backend-network:
driver: bridge
internal: true # No external accessVolume Management
Volume types:
- Named volumes: Managed by Docker, persist data
- Bind mounts: Map host directory to container (great for development)
- tmpfs mounts: Stored in memory, temporary data
# Named volume
docker run -v mydata:/app/data myimage
# Bind mount (development)
docker run -v $(pwd):/app myimage
# Read-only bind mount
docker run -v $(pwd)/config:/app/config:ro myimage
# List volumes
docker volume ls
# Remove unused volumes
docker volume pruneBest Practices
.dockerignore
# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
.env
.env.local
Dockerfile*
docker-compose*
README.md
.vscode
coverage
.nyc_output
*.test.js
*.spec.jsSecurity Best Practices
Keep your containers secure:
- Never run containers as root - create a non-root user
- Use specific image tags, not :latest
- Scan images for vulnerabilities: docker scan myimage
- Don't store secrets in images - use environment variables or secrets management
- Keep base images updated
- Use read-only file systems where possible
# Security-focused Dockerfile
FROM node:20-alpine
# Create non-root user
RUN addgroup -g 1001 -S appgroup && \
adduser -S appuser -u 1001 -G appgroup
WORKDIR /app
# Change ownership
COPY --chown=appuser:appgroup package*.json ./
RUN npm ci --only=production
COPY --chown=appuser:appgroup . .
# Switch to non-root user
USER appuser
# Read-only root filesystem
# (set in docker run: --read-only)
EXPOSE 3000
CMD ["node", "server.js"]Development Workflow
Development Dockerfile
# Dockerfile.dev
FROM node:20-alpine
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm install
# Don't copy code - we'll use bind mount
# This allows hot reloading
EXPOSE 3000
# Use nodemon for auto-restart
CMD ["npm", "run", "dev"]Makefile for Common Commands
# Makefile
.PHONY: dev build up down logs clean
dev:
docker-compose -f docker-compose.dev.yml up -d
build:
docker-compose build --no-cache
up:
docker-compose up -d
down:
docker-compose down
logs:
docker-compose logs -f
clean:
docker-compose down -v --rmi local
docker system prune -f
shell-backend:
docker-compose exec backend sh
shell-db:
docker-compose exec db psql -U user -d myappHow Do You Debug Containers?
# Execute shell in running container
docker exec -it container_name sh
# View container logs
docker logs -f container_name
# Inspect container details
docker inspect container_name
# View resource usage
docker stats
# View container processes
docker top container_name
# Copy files from container
docker cp container_name:/app/logs ./logsHow Do You Deploy Containers to Production?
Health Checks
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm ci --only=production
# Add health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
EXPOSE 3000
CMD ["node", "server.js"]Docker Compose Production
# docker-compose.prod.yml
version: '3.8'
services:
app:
image: myregistry/myapp:latest
deploy:
replicas: 3
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"Useful Commands Cheatsheet
# Image management
docker build -t myapp:v1 .
docker tag myapp:v1 registry/myapp:v1
docker push registry/myapp:v1
# Cleanup
docker system prune -a # Remove all unused data
docker image prune # Remove unused images
docker container prune # Remove stopped containers
docker volume prune # Remove unused volumes
# Resource monitoring
docker stats --all
docker system df # Disk usage
# Registry
docker login registry.example.com
docker pull registry.example.com/myapp:latestDocker is essential for modern development. Start with simple Dockerfiles, use Docker Compose for multi-service apps, implement multi-stage builds for production, and always follow security best practices. Containerization makes your apps portable, scalable, and consistent.
Master these Docker concepts and you'll have a solid foundation for container orchestration with Kubernetes, CI/CD pipelines, and cloud-native development.
