Docker has revolutionized how we build, ship, and run applications. Instead of "works on my machine," containers guarantee consistency from development to production. For DevOps engineers, Docker is non-negotiable—it's how we package applications, run CI/CD pipelines, and deploy to Kubernetes. In this hands-on guide, you'll learn Docker from scratch and containerize a real application.

1. Learning Objectives

By the end of this lesson, you will be able to:

  • Understand containers vs virtual machines
  • Pull and run Docker images
  • Write Dockerfiles to containerize applications
  • Build and tag custom images
  • Use volumes for persistent data
  • Expose network ports and link containers
  • Containerize a Python web application

2. Why Docker Matters for DevOps

Real-world scenario: Your Python app works perfectly on your laptop. You deploy to production—crash. Why? Different Python versions, missing system dependencies, environment variables not set. Docker solves this by packaging EVERYTHING: your code, runtime, system tools, and libraries. The container runs identically everywhere.

Why every DevOps engineer needs Docker:

  • ✅ Consistent environments (dev = staging = production)
  • ✅ Fast CI/CD (spin up containers for testing)
  • ✅ Microservices architecture (each service in its own container)
  • ✅ Reproducible infrastructure (Dockerfiles as code)
  • ✅ Foundation for Kubernetes (containers are the unit of deployment)

3. Core Concepts

Containers vs Virtual Machines

Virtual Machines: Each VM runs a full OS (Linux/Windows) with its own kernel. Heavy, slow to start (minutes), large disk usage (GBs).

Containers: Share the host OS kernel, only package the application and its dependencies. Lightweight, fast to start (seconds), small disk usage (MBs).

Docker Installation

# Ubuntu/Debian
sudo apt update
sudo apt install docker.io
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -aG docker $USER  # Add user to docker group

# macOS - Install Docker Desktop from docker.com

# Windows - Install Docker Desktop with WSL2

# Verify installation
docker --version
docker run hello-world
Installing Docker on different platforms
kubectl get pods -w
$ docker --version
Docker version 24.0.7, build 24.0.7-0ubuntu2

$ docker run hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.
Docker successfully installed

Docker Images and Containers

Image: A template/blueprint (like a class in programming).
Container: A running instance of an image (like an object).
Registry: Where images are stored (Docker Hub is the default).

# Pull an image from Docker Hub
docker pull nginx:latest

# List local images
docker images

# Run a container from an image
docker run nginx:latest

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

# List running containers
docker ps

# List all containers (including stopped)
docker ps -a

# Stop a container
docker stop container_id_or_name

# Start a stopped container
docker start container_id_or_name

# Remove a container
docker rm container_id_or_name

# Remove an image
docker rmi nginx:latest
Essential Docker commands

Port Mapping: Accessing Containers

# Run nginx and map host port 8080 to container port 80
docker run -d -p 8080:80 nginx:latest

# Visit http://localhost:8080 in browser
# You should see the nginx welcome page

# Multiple port mappings
docker run -d -p 8080:80 -p 8443:443 nginx:latest

# Port mapping with explicit IP
docker run -d -p 127.0.0.1:8080:80 nginx:latest

# Random port assignment
docker run -d -P nginx:latest

# Check mapped ports
docker port container_id
Exposing container ports to the host
kubectl get pods -w
$ docker run -d -p 8080:80 nginx
b8f2a1c3e4f5...

$ curl http://localhost:8080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
Nginx running on port 8080

Dockerfile: Building Custom Images

# Dockerfile - Multi-stage build for Python app

# Stage 1: Build stage
FROM python:3.11-slim AS builder

WORKDIR /app

# Install build dependencies
RUN apt-get update && apt-get install -y \
    gcc \
    && rm -rf /var/lib/apt/lists/*

# Copy requirements and install Python dependencies
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# Stage 2: Runtime stage
FROM python:3.11-slim

# Create non-root user
RUN useradd --create-home appuser

WORKDIR /app

# Copy Python dependencies from builder
COPY --from=builder /root/.local /home/appuser/.local

# Copy application code
COPY --chown=appuser:appuser . .

# Switch to non-root user
USER appuser

# Add local Python bin to PATH
ENV PATH=/home/appuser/.local/bin:$PATH

# Expose port
EXPOSE 8000

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

# Run the application
CMD ["python", "app.py"]
Production-ready Dockerfile
# app.py - Simple Flask web application
from flask import Flask, jsonify
import datetime

app = Flask(__name__)

@app.route('/')
def hello():
    return jsonify({
        'message': 'Hello from Docker!',
        'timestamp': datetime.datetime.now().isoformat(),
        'container': 'running'
    })

@app.route('/health')
def health():
    return jsonify({'status': 'healthy'}), 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)
Simple Flask app to containerize
# requirements.txt
flask==2.3.0
Python dependencies
# Build the image
docker build -t my-python-app:latest .

# Build with specific tag
docker build -t my-python-app:v1.0 .

# Build without cache
docker build --no-cache -t my-python-app:latest .

# Run the container
docker run -d -p 8000:8000 my-python-app:latest

# Test it
curl http://localhost:8000
Building and running custom images

Docker Volumes: Persistent Data

# Create a volume
docker volume create mydata

# List volumes
docker volume ls

# Inspect volume
docker volume inspect mydata

# Mount volume to container
docker run -d -v mydata:/data alpine tail -f /dev/null

# Bind mount (mount host directory)
docker run -d -v /home/user/data:/app/data myapp

# Mount with :ro for read-only
docker run -d -v mydata:/data:ro alpine

# Remove unused volumes
docker volume prune
Managing persistent storage

Docker Networking

# List networks
docker network ls

# Create custom network
docker network create mynetwork

# Run containers on custom network
docker run -d --name app1 --network mynetwork nginx
docker run -d --name app2 --network mynetwork nginx

# Containers can communicate by name
# app1 can ping app2

# Inspect network
docker network inspect mynetwork

# Connect existing container to network
docker network connect mynetwork app3

# Disconnect container
docker network disconnect mynetwork app3

# Remove network
docker network rm mynetwork
Container networking basics

4. Docker Compose: Multi-Container Applications

# docker-compose.yml
version: '3.8'

services:
  # Web application
  web:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://postgres:password@db:5432/app
      - REDIS_URL=redis://redis:6379
    depends_on:
      - db
      - redis
    volumes:
      - ./app:/app
    networks:
      - app-network

  # PostgreSQL database
  db:
    image: postgres:15
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=app
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - app-network

  # Redis cache
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    networks:
      - app-network

  # Nginx reverse proxy
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - web
    networks:
      - app-network

volumes:
  postgres_data:

networks:
  app-network:
    driver: bridge
Complete Docker Compose configuration
# Start all services
docker-compose up

# Start in background
docker-compose up -d

# Build images before starting
docker-compose up --build

# Stop all services
docker-compose down

# Stop and remove volumes (deletes data)
docker-compose down -v

# View logs
docker-compose logs

# Follow logs
docker-compose logs -f

# Execute command in service
docker-compose exec web python manage.py migrate

# List services
docker-compose ps

# Restart a service
docker-compose restart web
Docker Compose commands

5. Complete Project: Containerized Web Application

# Complete example with PostgreSQL and Redis
# app.py

import os
import redis
import psycopg2
from datetime import datetime
from flask import Flask, jsonify

app = Flask(__name__)

# Configure connections
redis_client = redis.Redis(host='redis', port=6379, decode_responses=True)

def get_db_connection():
    return psycopg2.connect(
        host='db',
        database='app',
        user='postgres',
        password='password'
    )

@app.route('/')
def home():
    # Increment visit counter in Redis
    visit_count = redis_client.incr('visits')
    
    # Get current time from PostgreSQL
    conn = get_db_connection()
    cur = conn.cursor()
    cur.execute("SELECT NOW()")
    current_time = cur.fetchone()[0]
    cur.close()
    conn.close()
    
    return jsonify({
        'message': 'Hello from Containerized App!',
        'visits': visit_count,
        'server_time': current_time.isoformat(),
        'container_id': os.uname().nodename
    })

@app.route('/health')
def health():
    # Check Redis
    try:
        redis_client.ping()
        redis_ok = True
    except:
        redis_ok = False
    
    # Check PostgreSQL
    try:
        conn = get_db_connection()
        conn.close()
        db_ok = True
    except:
        db_ok = False
    
    status = 'healthy' if (redis_ok and db_ok) else 'unhealthy'
    code = 200 if status == 'healthy' else 503
    
    return jsonify({
        'status': status,
        'redis': redis_ok,
        'database': db_ok
    }), code

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)
Production-ready multi-service application
# Initialize PostgreSQL table
# Wait for DB to be ready
sleep 10

# Create table
cat << EOF | docker-compose exec -T db psql -U postgres -d app
CREATE TABLE IF NOT EXISTS visits (
    id SERIAL PRIMARY KEY,
    visited_at TIMESTAMP DEFAULT NOW()
);
EOF

# Test the application
curl http://localhost:8000
curl http://localhost:8000/health

# Scale the web service
docker-compose up -d --scale web=3

# See all instances handling traffic
docker-compose logs web
Deploy and test multi-container app

6. Best Practices & Optimizations

# Optimized build commands
docker build --target builder -t myapp:builder .
docker build --target runtime -t myapp:latest .

# Image size comparison
docker images | grep myapp

# Scan for vulnerabilities
docker scan myapp:latest

# Set resource limits
docker run -d --memory=512m --cpus=0.5 myapp

# Clean up
docker system prune -a --volumes
Docker optimization commands

7. Common Errors & Solutions

8. Summary Checklist

9. Next Steps

Next lesson: Kubernetes: Container Orchestration

You'll learn to:

  • Deploy containers at scale with Kubernetes
  • Manage pods, deployments, and services
  • Implement rolling updates and rollbacks
  • Use ConfigMaps and Secrets for configuration

Practice resources:

Gataya Med

DevOps Engineer & Backend Developer. Sharing insights on cloud, automation, and scalable systems.

Comments (0)

Sarah Chen January 6, 2025

This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!

Reply

Leave a Comment