Skip to content

Docker Tutorial

Learn Docker containerization from basics to advanced concepts, including best practices for development and production environments.

Overview

Docker is a platform that uses containerization technology to package applications and their dependencies into lightweight, portable containers that can run consistently across different environments.

Docker Fundamentals

What is Docker?

  • Containerization: Package applications with all dependencies
  • Portability: Run anywhere Docker is installed
  • Isolation: Applications run in isolated environments
  • Efficiency: Share OS kernel, lightweight compared to VMs
  • Scalability: Easy to scale applications horizontally

Docker Architecture

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Docker Client │    │   Docker Host   │    │ Docker Registry │
│                 │    │                 │    │                 │
│ docker build    │───▶│ Docker Daemon   │◀──▶│   Docker Hub    │
│ docker pull     │    │                 │    │   Private Reg   │
│ docker run      │    │ ┌─────────────┐ │    │                 │
└─────────────────┘    │ │ Containers  │ │    └─────────────────┘
                       │ │ Images      │ │
                       │ │ Networks    │ │
                       │ │ Volumes     │ │
                       │ └─────────────┘ │
                       └─────────────────┘

Core Concepts

  • Image: Read-only template used to create containers
  • Container: Running instance of an image
  • Dockerfile: Text file with instructions to build an image
  • Registry: Storage and distribution system for Docker images
  • Volume: Persistent data storage for containers
  • Network: Communication between containers

Getting Started

Installation

bash
# Ubuntu/Debian
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER

# macOS (using Homebrew)
brew install --cask docker

# Windows
# Download Docker Desktop from docker.com

# Verify installation
docker --version
docker run hello-world

Basic Commands

bash
# Image management
docker images                    # List images
docker pull nginx:latest         # Pull image from registry
docker build -t myapp:1.0 .     # Build image from Dockerfile
docker rmi image_id              # Remove image

# Container management
docker ps                       # List running containers
docker ps -a                    # List all containers
docker run -d -p 80:80 nginx   # Run container in background
docker stop container_id        # Stop container
docker start container_id       # Start stopped container
docker rm container_id          # Remove container

# Container interaction
docker exec -it container_id bash  # Execute command in container
docker logs container_id           # View container logs
docker inspect container_id        # Inspect container details

Creating Docker Images

Simple Dockerfile

dockerfile
# Use official Node.js runtime as base image
FROM node:18-alpine

# Set working directory
WORKDIR /app

# Copy package.json and package-lock.json
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy application code
COPY . .

# Expose port
EXPOSE 3000

# Define command to run application
CMD ["npm", "start"]

Multi-stage Build

dockerfile
# Build stage
FROM node:18-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

# Production stage
FROM node:18-alpine AS production

# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nextjs -u 1001

WORKDIR /app

# Copy built application from builder stage
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json

# Switch to non-root user
USER nextjs

EXPOSE 3000

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

CMD ["npm", "start"]

Dockerfile Best Practices

dockerfile
# Use specific version tags
FROM node:18.17.0-alpine

# Use .dockerignore to exclude unnecessary files
# .dockerignore content:
# node_modules
# npm-debug.log
# .git
# .gitignore
# README.md
# .env
# coverage
# .nyc_output

# Minimize layers by combining RUN commands
RUN apk add --no-cache \
    python3 \
    make \
    g++ \
    && npm ci --only=production \
    && npm cache clean --force

# Use COPY instead of ADD when possible
COPY package*.json ./
COPY src/ ./src/

# Set appropriate file permissions
COPY --chown=node:node . .

# Use specific EXPOSE ports
EXPOSE 3000

# Use exec form for CMD and ENTRYPOINT
CMD ["node", "server.js"]

# Add labels for metadata
LABEL maintainer="your-email@example.com"
LABEL version="1.0"
LABEL description="My Node.js application"

Container Management

Running Containers

bash
# Basic run
docker run nginx

# Run in background (detached)
docker run -d nginx

# Run with port mapping
docker run -d -p 8080:80 nginx

# Run with environment variables
docker run -d -e NODE_ENV=production -e PORT=3000 myapp

# Run with volume mount
docker run -d -v /host/path:/container/path nginx

# Run with custom name
docker run -d --name my-nginx nginx

# Run with resource limits
docker run -d --memory=512m --cpus=0.5 nginx

# Run with restart policy
docker run -d --restart=unless-stopped nginx

Container Networking

bash
# List networks
docker network ls

# Create custom network
docker network create mynetwork

# Run container on custom network
docker run -d --network=mynetwork --name web nginx
docker run -d --network=mynetwork --name db postgres

# Connect existing container to network
docker network connect mynetwork container_name

# Inspect network
docker network inspect mynetwork

Data Management with Volumes

bash
# Create named volume
docker volume create mydata

# List volumes
docker volume ls

# Use named volume
docker run -d -v mydata:/data nginx

# Use bind mount
docker run -d -v /host/path:/container/path nginx

# Use tmpfs mount (in-memory)
docker run -d --tmpfs /tmp nginx

# Inspect volume
docker volume inspect mydata

# Remove unused volumes
docker volume prune

Docker Compose

Basic docker-compose.yml

yaml
version: '3.8'

services:
  web:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://user:password@db:5432/myapp
    depends_on:
      - db
      - redis
    volumes:
      - .:/app
      - /app/node_modules
    networks:
      - app-network

  db:
    image: postgres:15
    environment:
      - POSTGRES_DB=myapp
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - app-network

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    networks:
      - app-network

volumes:
  postgres_data:
  redis_data:

networks:
  app-network:
    driver: bridge

Advanced Compose Configuration

yaml
version: '3.8'

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile.prod
      args:
        - NODE_ENV=production
    ports:
      - "80:3000"
    environment:
      - NODE_ENV=production
    env_file:
      - .env.production
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  db:
    image: postgres:15
    environment:
      - POSTGRES_DB=myapp
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD_FILE=/run/secrets/db_password
    secrets:
      - db_password
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
      interval: 10s
      timeout: 5s
      retries: 5

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - web

secrets:
  db_password:
    file: ./secrets/db_password.txt

volumes:
  postgres_data:

Compose Commands

bash
# Start services
docker-compose up
docker-compose up -d  # Run in background

# Build and start
docker-compose up --build

# Stop services
docker-compose down

# Stop and remove volumes
docker-compose down -v

# View logs
docker-compose logs
docker-compose logs web  # Specific service

# Scale services
docker-compose up --scale web=3

# Execute command in service
docker-compose exec web bash

# View running services
docker-compose ps

Production Best Practices

Security Hardening

dockerfile
# Use non-root user
FROM node:18-alpine

# Create app user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nextjs -u 1001

# Set working directory
WORKDIR /app

# Copy and install dependencies as root
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

# Copy application code
COPY --chown=nextjs:nodejs . .

# Switch to non-root user
USER nextjs

# Use read-only root filesystem
# docker run --read-only --tmpfs /tmp myapp

# Drop capabilities
# docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myapp

EXPOSE 3000
CMD ["node", "server.js"]

Image Optimization

dockerfile
# Use Alpine Linux for smaller images
FROM node:18-alpine

# Multi-stage build to reduce final image size
FROM node:18-alpine AS dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:18-alpine AS runtime
WORKDIR /app
COPY --from=dependencies /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package*.json ./

# Remove unnecessary packages
RUN apk del .build-deps

EXPOSE 3000
CMD ["npm", "start"]

Health Checks

dockerfile
# Application health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

# Custom health check script
COPY healthcheck.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/healthcheck.sh
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
  CMD /usr/local/bin/healthcheck.sh
bash
#!/bin/sh
# healthcheck.sh
set -e

# Check if application is responding
if ! curl -f http://localhost:3000/health; then
    exit 1
fi

# Check database connection
if ! nc -z db 5432; then
    exit 1
fi

# Check Redis connection
if ! nc -z redis 6379; then
    exit 1
fi

echo "Health check passed"
exit 0

Logging Configuration

yaml
# docker-compose.yml
version: '3.8'

services:
  web:
    image: myapp:latest
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
        labels: "service=web"
    
  # Centralized logging with ELK stack
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.8.0
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
    volumes:
      - elasticsearch_data:/usr/share/elasticsearch/data

  logstash:
    image: docker.elastic.co/logstash/logstash:8.8.0
    volumes:
      - ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf
    depends_on:
      - elasticsearch

  kibana:
    image: docker.elastic.co/kibana/kibana:8.8.0
    ports:
      - "5601:5601"
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
    depends_on:
      - elasticsearch

volumes:
  elasticsearch_data:

Container Orchestration

Docker Swarm

bash
# Initialize swarm
docker swarm init

# Join worker nodes
docker swarm join --token SWMTKN-1-... manager-ip:2377

# Deploy stack
docker stack deploy -c docker-compose.yml myapp

# List services
docker service ls

# Scale service
docker service scale myapp_web=5

# Update service
docker service update --image myapp:v2 myapp_web

# Remove stack
docker stack rm myapp

Swarm Compose File

yaml
version: '3.8'

services:
  web:
    image: myapp:latest
    ports:
      - "80:3000"
    deploy:
      replicas: 3
      update_config:
        parallelism: 1
        delay: 10s
        failure_action: rollback
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
      placement:
        constraints:
          - node.role == worker
    networks:
      - webnet

  db:
    image: postgres:15
    environment:
      - POSTGRES_DB=myapp
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD_FILE=/run/secrets/db_password
    secrets:
      - db_password
    volumes:
      - db_data:/var/lib/postgresql/data
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.role == manager
    networks:
      - webnet

secrets:
  db_password:
    external: true

volumes:
  db_data:

networks:
  webnet:
    driver: overlay

Monitoring and Debugging

Container Monitoring

bash
# Monitor resource usage
docker stats

# Monitor specific container
docker stats container_name

# View container processes
docker top container_name

# Inspect container details
docker inspect container_name

# View container filesystem changes
docker diff container_name

Debugging Containers

bash
# View container logs
docker logs container_name
docker logs -f container_name  # Follow logs
docker logs --tail 100 container_name  # Last 100 lines

# Execute commands in running container
docker exec -it container_name bash
docker exec -it container_name sh  # For Alpine images

# Copy files to/from container
docker cp file.txt container_name:/path/to/destination
docker cp container_name:/path/to/file.txt ./

# Debug failed container
docker run -it --entrypoint /bin/sh image_name

Performance Monitoring

yaml
# docker-compose.monitoring.yml
version: '3.8'

services:
  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--web.console.libraries=/etc/prometheus/console_libraries'
      - '--web.console.templates=/etc/prometheus/consoles'

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:
      - grafana_data:/var/lib/grafana

  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    ports:
      - "8080:8080"
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
      - /dev/disk/:/dev/disk:ro

volumes:
  prometheus_data:
  grafana_data:

CI/CD Integration

GitHub Actions

yaml
# .github/workflows/docker.yml
name: Docker Build and Push

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
    - name: Checkout repository
      uses: actions/checkout@v3

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2

    - name: Log in to Container Registry
      uses: docker/login-action@v2
      with:
        registry: ${{ env.REGISTRY }}
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

    - name: Extract metadata
      id: meta
      uses: docker/metadata-action@v4
      with:
        images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
        tags: |
          type=ref,event=branch
          type=ref,event=pr
          type=sha,prefix={{branch}}-
          type=raw,value=latest,enable={{is_default_branch}}

    - name: Build and push Docker image
      uses: docker/build-push-action@v4
      with:
        context: .
        platforms: linux/amd64,linux/arm64
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}
        cache-from: type=gha
        cache-to: type=gha,mode=max

    - name: Run Trivy vulnerability scanner
      uses: aquasecurity/trivy-action@master
      with:
        image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
        format: 'sarif'
        output: 'trivy-results.sarif'

    - name: Upload Trivy scan results
      uses: github/codeql-action/upload-sarif@v2
      with:
        sarif_file: 'trivy-results.sarif'

GitLab CI/CD

yaml
# .gitlab-ci.yml
stages:
  - build
  - test
  - security
  - deploy

variables:
  DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  DOCKER_LATEST: $CI_REGISTRY_IMAGE:latest

before_script:
  - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY

build:
  stage: build
  script:
    - docker build -t $DOCKER_IMAGE -t $DOCKER_LATEST .
    - docker push $DOCKER_IMAGE
    - docker push $DOCKER_LATEST

test:
  stage: test
  script:
    - docker run --rm $DOCKER_IMAGE npm test

security_scan:
  stage: security
  script:
    - docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
        aquasec/trivy image --exit-code 1 --severity HIGH,CRITICAL $DOCKER_IMAGE

deploy:
  stage: deploy
  script:
    - docker-compose -f docker-compose.prod.yml up -d
  only:
    - main

Troubleshooting

Common Issues

bash
# Container exits immediately
docker logs container_name  # Check logs for errors
docker run -it image_name /bin/sh  # Debug interactively

# Port already in use
docker ps  # Check running containers
sudo netstat -tulpn | grep :port  # Check what's using the port

# Permission denied
# Add user to docker group
sudo usermod -aG docker $USER
# Logout and login again

# Out of disk space
docker system prune  # Remove unused data
docker system prune -a  # Remove all unused data
docker volume prune  # Remove unused volumes

# Image build fails
docker build --no-cache -t myapp .  # Build without cache
docker build --progress=plain -t myapp .  # Verbose output

Performance Issues

bash
# Check resource usage
docker stats

# Limit container resources
docker run -m 512m --cpus=0.5 myapp

# Use multi-stage builds to reduce image size
# Use .dockerignore to exclude unnecessary files
# Use Alpine Linux base images
# Combine RUN commands to reduce layers

Cleanup Commands

bash
# Remove stopped containers
docker container prune

# Remove unused images
docker image prune
docker image prune -a  # Remove all unused images

# Remove unused volumes
docker volume prune

# Remove unused networks
docker network prune

# Remove everything unused
docker system prune -a --volumes

# Remove specific items
docker rm $(docker ps -aq)  # Remove all containers
docker rmi $(docker images -q)  # Remove all images

This comprehensive Docker tutorial covers containerization from basics to production deployment. Practice with real applications to master these concepts.

VitePress Development Guide