Docker for Developers: A Practical Guide to Containerization

Master Docker fundamentals with hands-on examples. Learn containers, images, volumes, networking, and Docker Compose for local development and production deployment.

Building & Shipping: Docker for Developers: A Practical Guide to Containerization

Why Docker Matters

Docker solves the “works on my machine” problem by packaging applications with their dependencies into containers. Whether you’re building microservices, deploying to the cloud, or just want consistent development environments, Docker is essential.

Understanding Containers vs Virtual Machines

Virtual Machines:

  • Full OS per VM
  • Heavy resource usage (GBs of RAM, disk)
  • Slow to start (minutes)
  • Strong isolation

Containers:

  • Share host OS kernel
  • Lightweight (MBs)
  • Fast startup (seconds)
  • Process-level isolation

When to use what: VMs for complete isolation and different OS requirements; containers for application deployment and microservices.

Docker Architecture

Docker Client (docker CLI)

   Docker Daemon

    ┌───┴───┐
Images  Containers
  • Images: Read-only templates (like classes)
  • Containers: Running instances of images (like objects)
  • Dockerfile: Instructions to build an image
  • Docker Hub: Public registry for images

Your First Container

# Run a container
docker run hello-world

# Run interactively
docker run -it ubuntu bash

# Run in background
docker run -d nginx

# Port mapping
docker run -d -p 8080:80 nginx
# Access at http://localhost:8080

Essential Docker Commands

Container Management

# 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 logs
docker logs <container-id>

# Execute command in running container
docker exec -it <container-id> bash

Image Management

# List images
docker images

# Pull an image
docker pull node:18

# Remove an image
docker rmi <image-id>

# Build an image
docker build -t myapp:1.0 .

# Tag an image
docker tag myapp:1.0 myapp:latest

Writing Dockerfiles

Node.js Application

# Use official Node.js image
FROM node:18-alpine

# Set working directory
WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy application code
COPY . .

# Expose port
EXPOSE 3000

# Set user (security best practice)
USER node

# Start application
CMD ["node", "server.js"]

Multi-Stage Build (Smaller Images)

# 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
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]

Benefits:

  • Smaller final image (no build tools)
  • Faster deployment
  • More secure (fewer dependencies)

Dockerfile Best Practices

1. Layer Caching

# ❌ Bad: Invalidates cache on any code change
COPY . .
RUN npm install

# ✅ Good: Cache dependencies separately
COPY package*.json ./
RUN npm ci
COPY . .

2. Use .dockerignore

node_modules
npm-debug.log
.git
.env
*.md
.vscode
.idea
dist
coverage

3. Alpine for Smaller Images

# Large: 900MB
FROM node:18

# Small: 170MB
FROM node:18-alpine

4. Non-Root User

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

# Or use built-in node user
USER node

5. Healthchecks

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
  CMD node healthcheck.js

Docker Volumes: Persisting Data

# Named volume
docker run -v mydata:/app/data myapp

# Bind mount (development)
docker run -v $(pwd):/app myapp

# List volumes
docker volume ls

# Inspect volume
docker volume inspect mydata

# Remove unused volumes
docker volume prune

When to use what:

  • Named volumes: Production databases, persistent app data
  • Bind mounts: Local development (live code reloading)
  • tmpfs: Temporary data, sensitive info that shouldn’t persist

Docker Networks

# Create network
docker network create mynetwork

# Run containers on same network
docker run -d --network mynetwork --name db postgres
docker run -d --network mynetwork --name api myapi

# API can access DB at hostname "db"

Network Types

  • bridge: Default, isolated network
  • host: Use host’s network (no isolation)
  • none: No networking
  • Custom bridge: Named networks with DNS

Docker Compose: Multi-Container Apps

docker-compose.yml

version: '3.8'

services:
  # Database
  db:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: password
      POSTGRES_DB: myapp
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
  
  # Redis cache
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
  
  # API server
  api:
    build: ./api
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://postgres:password@db:5432/myapp
      REDIS_URL: redis://redis:6379
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    volumes:
      - ./api:/app
      - /app/node_modules
    develop:
      watch:
        - path: ./api
          action: sync
          target: /app

volumes:
  db-data:

Compose Commands

# Start all services
docker-compose up

# Start in background
docker-compose up -d

# Rebuild images
docker-compose up --build

# Stop services
docker-compose down

# Stop and remove volumes
docker-compose down -v

# View logs
docker-compose logs -f api

# Execute command
docker-compose exec api sh

Development Workflow

Hot Reload Setup

services:
  dev:
    build:
      context: .
      target: development
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      NODE_ENV: development
    command: npm run dev

Multi-Stage Dockerfile

# Development stage
FROM node:18-alpine AS development
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]

# Production stage
FROM node:18-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD ["node", "server.js"]

Production Deployment

1. Build Optimized Image

# Multi-stage build
docker build -t myapp:1.0 .

# Check image size
docker images myapp:1.0

2. Security Scanning

# Scan for vulnerabilities
docker scan myapp:1.0

# Or use Trivy
trivy image myapp:1.0

3. Push to Registry

# Tag for registry
docker tag myapp:1.0 registry.example.com/myapp:1.0

# Push
docker push registry.example.com/myapp:1.0

4. Production Compose

version: '3.8'

services:
  api:
    image: registry.example.com/myapp:1.0
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: production
      DATABASE_URL_FILE: /run/secrets/db_url
    secrets:
      - db_url
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: '0.5'
          memory: 512M

secrets:
  db_url:
    external: true

Common Patterns

Database Initialization

services:
  db:
    image: postgres:15
    volumes:
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql

Wait for Service

# Install wait-for-it
COPY --from=wait-for-it:latest /wait-for-it.sh /usr/local/bin/

CMD ["/usr/local/bin/wait-for-it.sh", "db:5432", "--", "node", "server.js"]

Environment Variables from File

services:
  api:
    env_file:
      - .env
      - .env.local

Debugging Containers

Inspect Container

# View container details
docker inspect <container-id>

# View specific field
docker inspect -f '{{.State.Status}}' <container-id>

# View network settings
docker inspect -f '{{.NetworkSettings.IPAddress}}' <container-id>

Debug Running Container

# Shell into container
docker exec -it <container-id> sh

# View processes
docker top <container-id>

# View resource usage
docker stats <container-id>

# View logs
docker logs -f --tail 100 <container-id>

Debug Build Issues

# Build with no cache
docker build --no-cache -t myapp .

# See build context
docker build --progress=plain -t myapp .

# Run specific stage
docker build --target development -t myapp:dev .

Performance Optimization

1. Layer Optimization

# Combine commands to reduce layers
RUN apt-get update && apt-get install -y \
    package1 \
    package2 \
    && rm -rf /var/lib/apt/lists/*

2. Build Cache

# Use BuildKit for better caching
DOCKER_BUILDKIT=1 docker build -t myapp .

3. Image Registry Caching

# Pull cache from registry
FROM --platform=$BUILDPLATFORM registry.example.com/myapp:cache AS cache

Troubleshooting

Container Exits Immediately

# Check exit code
docker ps -a

# View logs
docker logs <container-id>

# Try running with shell
docker run -it myapp sh

Out of Disk Space

# Clean up
docker system prune -a --volumes

# View disk usage
docker system df

Network Issues

# Test connectivity
docker run --network mynetwork alpine ping db

# Inspect network
docker network inspect mynetwork

Docker in CI/CD

GitHub Actions

name: Build and Push

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Build image
        run: docker build -t myapp:${{ github.sha }} .
      
      - name: Push to registry
        run: |
          echo ${{ secrets.REGISTRY_PASSWORD }} | docker login -u ${{ secrets.REGISTRY_USERNAME }} --password-stdin
          docker push myapp:${{ github.sha }}

Best Practices Summary

  1. Use official base images from Docker Hub
  2. Multi-stage builds for smaller production images
  3. Run as non-root user for security
  4. .dockerignore to exclude unnecessary files
  5. Layer caching by ordering instructions properly
  6. Health checks for container monitoring
  7. Named volumes for persistent data
  8. Environment variables for configuration
  9. Docker Compose for local development
  10. Security scanning before production deployment

Next Steps

  • Learn Kubernetes for orchestration at scale
  • Explore Docker Swarm for simpler clustering
  • Study container security best practices
  • Implement monitoring with Prometheus/Grafana
  • Try Buildpacks as an alternative to Dockerfiles

Resources