Docker & Containerization: A Complete Guide for Developers
DevOps14 min read

Docker & Containerization: A Complete Guide for Developers

Master Docker from basics to advanced. Learn containers, images, Docker Compose, multi-stage builds, and production best practices.

Taha Kocal

Taha Kocal

Full Stack Developer

Mar 10, 2025
#Docker#DevOps#Containers#Microservices#Deployment

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.

bash
# 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-app

How Do You Write a Good Dockerfile?

Basic Node.js Dockerfile

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.

dockerfile
# 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

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

yaml
# 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

bash
# 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=3

Networking

Docker provides several network modes. In Docker Compose, services can communicate using service names as hostnames.

yaml
# 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 access

Volume 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
bash
# 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 prune

Best Practices

.dockerignore

text
# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
.env
.env.local
Dockerfile*
docker-compose*
README.md
.vscode
coverage
.nyc_output
*.test.js
*.spec.js

Security 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
dockerfile
# 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
# 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
# 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 myapp

How Do You Debug Containers?

bash
# 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 ./logs

How Do You Deploy Containers to Production?

Health Checks

dockerfile
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

yaml
# 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

bash
# 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:latest

Docker 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.

Share this article

Related Articles