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
$ 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 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
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
$ docker run -d -p 8080:80 nginx
b8f2a1c3e4f5...
$ curl http://localhost:8080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
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"]
# 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)
# requirements.txt
flask==2.3.0
# 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
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
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
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
# 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
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)
# 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
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
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:
- Play with Docker - Free online Docker environment
- DockerLabs - Free tutorials
- Docker Official Tutorial
Comments (0)
This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!
ReplyLeave a Comment