- Containerization with Docker
- Table of Contents
- Introduction to Docker
- Prerequisites
- Setting Up Your Environment
- Creating a Simple Flask Application
- Creating a Dockerfile
- Building and Running Docker Containers
- Docker Volumes for Persistent Data
- Docker Compose for Multi-Container Applications
- Docker Networking
- Docker Best Practices
- Debugging Docker Containers
- Docker in Production
- Troubleshooting Common Issues
Docker is an open-source platform that automates the deployment, scaling, and management of applications using containerization. Containers package an application with all its dependencies, libraries, and configuration files, ensuring it runs consistently across different environments.
Key Concepts:
- Container: A lightweight, standalone executable package that includes everything needed to run an application
- Image: A read-only template used to create containers
- Dockerfile: A text file with instructions for building a Docker image
- Registry: A repository for storing and distributing Docker images
- Volume: A mechanism for persisting data generated by containers
- Docker Compose: A tool for defining and running multi-container applications
Benefits of Docker:
- Consistent environments across development, testing, and production
- Isolation of applications and dependencies
- Efficient resource utilization compared to traditional virtual machines
- Faster deployment and scaling
- Simplified configuration and maintenance
Before starting with Docker containerization, ensure you have:
- A computer running Windows, macOS, or Linux
- Administrative or sudo privileges on your machine
- At least 4GB of RAM and 10GB of free disk space
- Internet connection for downloading Docker and images
- Basic understanding of command line operations
- Familiarity with the application you want to containerize
For macOS:
# Using Homebrew
brew install docker docker-compose
# Alternatively, download Docker Desktop from https://www.docker.com/products/docker-desktopFor Windows:
- Download Docker Desktop from https://www.docker.com/products/docker-desktop
- Install and follow the wizard instructions
- Ensure WSL 2 is enabled (for Windows 10/11)
For Ubuntu:
# Update package index
sudo apt-get update
# Install prerequisites
sudo apt-get install apt-transport-https ca-certificates curl software-properties-common
# Add Docker's official GPG key
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
# Add the Docker repository
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
# Update package index again
sudo apt-get update
# Install Docker CE
sudo apt-get install docker-ce docker-ce-cli containerd.io
# Start and enable Docker service
sudo systemctl start docker
sudo systemctl enable docker
# Add your user to the Docker group to run Docker without sudo
sudo usermod -aG docker $USER
# Log out and back in for changes to take effect# Check Docker version
docker --version
# Run a test container
docker run hello-worldIf you see a "Hello from Docker!" message, the installation was successful.
We'll create a simple Flask web application as our example for containerization.
# Create and navigate to project directory
mkdir flask-docker
cd flask-dockertouch app.pyCreate app.py:
from flask import Flask, jsonify, render_template
import os
import socket
app = Flask(__name__)
@app.route('/')
def hello():
hostname = socket.gethostname()
return jsonify({
"message": "Hello from Docker!",
"hostname": hostname,
"environment": os.environ.get("ENVIRONMENT", "development")
})
@app.route('/health')
def health():
return jsonify({"status": "ok"})
@app.route('/ui')
def ui():
hostname = socket.gethostname()
return render_template('index.html', hostname=hostname)
if __name__ == '__main__':
port = int(os.environ.get("PORT", 5000))
app.run(host='0.0.0.0', port=port, debug=os.environ.get("DEBUG", "true").lower() == "true")Create requirements.txt:
touch requirements.txtflask==2.2.3
werkzeug==2.2.3
gunicorn==20.1.0
Create templates/index.html:
# First create the templates directory
mkdir -p templates
cd templates
touch index.html<!DOCTYPE html>
<html>
<head>
<title>Docker Flask App</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 40px;
line-height: 1.6;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 5px;
}
.info {
background-color: #f4f4f4;
padding: 15px;
border-radius: 5px;
}
</style>
</head>
<body>
<div class="container">
<h1>Hello from Docker!</h1>
<div class="info">
<p>This page is served from container: <strong>{{ hostname }}</strong></p>
</div>
</div>
</body>
</html>A Dockerfile contains instructions for building a Docker image. Let's create one for our Flask application:
touch DockerfileCreate Dockerfile:
# Use an official Python runtime as a parent image
FROM python:3.10-slim
# Set working directory
WORKDIR /app
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PORT=5000 \
ENVIRONMENT=production \
DEBUG=false
# Copy requirements first for better cache
COPY requirements.txt .
# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy the rest of the application
COPY . .
# Create a non-root user and switch to it
RUN adduser --disabled-password --gecos '' appuser && \
chown -R appuser:appuser /app
USER appuser
# Make port 5000 available
EXPOSE 5000
# Run the application with Gunicorn
CMD gunicorn --bind 0.0.0.0:$PORT --workers 2 --threads 4 --timeout 60 "app:app"Create .dockerignore file:
touch .dockerignoreconpy and pase the following:
.git
.gitignore
Dockerfile
docker-compose.yml
README.md
__pycache__
*.pyc
*.pyo
*.pyd
.Python
env/
venv/
.env
.venv
data/
*.log
# Build the image
docker build -t flask-app:v1 .
# List available images
docker imagesRun in foreground mode:
docker run -p 5111:5000 flask-app:v1The docker run -p 5111:5000 flask-app:v1 command demonstrates Docker port binding, which creates a mapping between ports on your host machine and ports inside your container.
Let's break down what -p 5111:5000 means:
5111 is the available port on your host machine (your computer).
5000 is the port inside the Docker container where your Flask app is running
The colon : separates these two port numbers
So when you run this command, Docker:
Starts a container from the flask-app:v1 image Sets up networking so that requests to localhost:5111 on your host machine are forwarded to port 5000 inside the container
This port binding allows you to:
Run multiple containers using the same internal port (e.g., 5000) but map them to different host ports Access your containerized application from your browser or other tools using http://localhost:5111 Keep container ports isolated from each other and your host system
Without this port mapping, the Flask application would still run on port 5000 inside the container, but you wouldn't be able to access it from outside the container.
Run in detached mode:
docker run -d -p 5111:5000 --name my-flask-app flask-app:v1Run with environment variables:
docker run -d -p 5111:5000 \
-e DEBUG=true \
-e ENVIRONMENT=development \
--name my-dev-app flask-app:v1Open your browser and enter 'localhost:5111'
# List running containers
docker ps
# List all containers (including stopped)
docker ps -a
# Stop a container
docker stop my-flask-app
# Start a stopped container
docker start my-flask-app
# Remove a container (must be stopped first)
docker rm my-flask-app
# Run and automatically remove when stopped
docker run --rm -p 5111:5000 flask-app:v1
# View container logs
docker logs my-flask-app
# Follow container logs
docker logs -f my-flask-app
# Execute commands inside a running container
docker exec -it my-flask-app /bin/bashDocker volumes allow you to persist data beyond the lifecycle of a container and share data between containers and the host system.
- Named Volumes: Managed by Docker, stored in a part of the host filesystem
- Bind Mounts: Directly map a host path to a container path
- tmpfs Mounts: Stored in host system's memory only (temporary)
Create a named volume:
docker volume create flask-dataRun a container with a named volume:
docker run -d -p 5111:5000 \
--name flask-with-volume \
-v flask-data:/app/data \
flask-app:v1Run a container with a bind mount:
# Create a directory for data
mkdir -p data
echo "This is persistent data" > data/test.txt
# Mount it to the container
docker run -d -p 5112:5000 \
--name flask-with-bind-mount \
-v $(pwd)/data:/app/data \
flask-app:v1Verify the mounted volume:
docker exec -it flask-with-bind-mount /bin/bash
cat /app/data/test.txt
exitList volumes:
docker volume lsInspect a volume:
docker volume inspect flask-dataRemove a volume:
docker volume rm flask-dataNote: You will need to stop the running comtainer before removing the volume.
Docker Compose simplifies the management of multi-container applications.
touch docker-compose.ymlCreate docker-compose.yml:
services:
web:
build:
context: .
dockerfile: Dockerfile
image: flask-app:latest
container_name: flask-web
restart: unless-stopped
ports:
- "5000:5000"
volumes:
- ./data:/app/data
environment:
- PORT=5000
- DEBUG=true
- ENVIRONMENT=development
depends_on:
- redis
networks:
- app-network
redis:
image: "redis:alpine"
container_name: flask-redis
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis-data:/data
command: redis-server --appendonly yes
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
redis-data:Update app.py to use Redis:
from flask import Flask, jsonify, render_template
import os
import socket
import redis
app = Flask(__name__)
# Connect to Redis (with graceful fallback if not available)
try:
redis_client = redis.Redis(host=os.environ.get('REDIS_HOST', 'redis'),
port=int(os.environ.get('REDIS_PORT', 6379)),
db=0,
socket_connect_timeout=2,
socket_timeout=2)
redis_connected = True
except:
redis_connected = False
@app.route('/')
def hello():
hostname = socket.gethostname()
counter = 0
if redis_connected:
try:
redis_client.incr('hits')
counter = int(redis_client.get('hits').decode('utf-8'))
except:
redis_connected = False
return jsonify({
"message": "Hello from Docker!",
"hostname": hostname,
"environment": os.environ.get("ENVIRONMENT", "development"),
"redis_connected": redis_connected,
"counter": counter
})
@app.route('/health')
def health():
redis_status = "connected" if redis_connected else "disconnected"
return jsonify({
"status": "ok",
"redis": redis_status
})
@app.route('/ui')
def ui():
hostname = socket.gethostname()
counter = 0
if redis_connected:
try:
redis_client.incr('hits')
counter = int(redis_client.get('hits').decode('utf-8'))
except:
pass
return render_template('index.html',
hostname=hostname,
counter=counter,
redis_connected=redis_connected)
if __name__ == '__main__':
port = int(os.environ.get("PORT", 5000))
app.run(host='0.0.0.0', port=port, debug=os.environ.get("DEBUG", "true").lower() == "true")Update requirements.txt:
flask==2.2.3
werkzeug==2.2.3
gunicorn==20.1.0
redis==4.5.1
Update templates/index.html:
<!DOCTYPE html>
<html>
<head>
<title>Docker Flask App</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 40px;
line-height: 1.6;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 5px;
}
.info {
background-color: #f4f4f4;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.counter {
font-size: 24px;
font-weight: bold;
color: #333;
}
.badge {
display: inline-block;
padding: 5px 10px;
border-radius: 20px;
font-size: 14px;
font-weight: bold;
}
.badge-success {
background-color: #28a745;
color: white;
}
.badge-danger {
background-color: #dc3545;
color: white;
}
</style>
</head>
<body>
<div class="container">
<h1>Hello from Docker!</h1>
<div class="info">
<p>This page is served from container: <strong>{{ hostname }}</strong></p>
<p>Redis:
{% if redis_connected %}
<span class="badge badge-success">Connected</span>
{% else %}
<span class="badge badge-danger">Disconnected</span>
{% endif %}
</p>
{% if redis_connected %}
<p>Page views: <span class="counter">{{ counter }}</span></p>
{% endif %}
</div>
</div>
</body>
</html># Start all services
docker compose up -d# View logs from all services
docker compose logs# View logs from a specific service
docker compose logs web
# Follow logs
docker compose logs -f# Check the status of services
docker compose ps# Stop all services but keep containers
docker compose stop
# Start services again
docker compose start
# Stop and remove containers, networks, and volumes
docker compose down
# Stop and remove containers, networks, volumes, and images
docker compose down --rmi all --volumesDocker provides several network drivers for different use cases.
- Bridge: Default network driver, suitable for standalone containers on a single host
- Host: Removes network isolation, using the host's network directly
- Overlay: Connects multiple Docker daemons, used in Docker Swarm
- Macvlan: Assigns a MAC address to a container, making it appear as a physical device
- None: Disables all networking for a container
# List networks
docker network ls
# Create a network
docker network create my-network
# Inspect a network
docker network inspect my-network
# Connect a running container to a network
docker network connect my-network my-flask-app
# Disconnect a container from a network
docker network disconnect my-network my-flask-app
# Run a container with a specific network
docker run -d --name my-flask-app --network my-network flask-app:v1
# Remove a network
docker network rm my-networknetworks:
frontend:
driver: bridge
ipam:
driver: default
config:
- subnet: 172.28.0.0/16
backend:
driver: bridge- Use specific image versions rather than the
latesttag - Scan images for vulnerabilities:
docker scan flask-app:v1
- Run containers as non-root users
- Use minimal base images like alpine or slim variants
- Don't store secrets in images - use environment variables or Docker secrets
- Use multi-stage builds to reduce image size
- Order your Dockerfile instructions to take advantage of layer caching
- Minimize the number of layers by combining commands
- Clean up in the same layer where you install packages
- Use .dockerignore to exclude unnecessary files
touch Dockerfile.multi# Build stage
FROM python:3.10-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# Run stage
FROM python:3.10-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PORT=5000
# Create a non-root user
RUN adduser --disabled-password --gecos '' appuser && \
chown -R appuser:appuser /app
USER appuser
EXPOSE 5000
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]# View container details
docker inspect my-flask-app
# View container logs
docker logs my-flask-app
# Get specific information using format
docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' my-flask-app# Interactive shell in a running container
docker exec -it my-flask-app /bin/bash
# If bash is not available, try sh
docker exec -it my-flask-app /bin/sh
# Monitor container resource usage
docker stats my-flask-app
# Copy files between container and host
docker cp my-flask-app:/app/logs/app.log ./local-app.log
docker cp ./local-config.json my-flask-app:/app/config.json# Install network tools in a container
docker exec -it my-flask-app /bin/bash
apt-get update && apt-get install -y iputils-ping net-tools curl
# Check connectivity
ping redis
curl http://redis:6379For production environments, consider using container orchestration platforms:
- Docker Swarm: Simple built-in orchestration for Docker
- Kubernetes: Powerful, industry-standard container orchestration
- Amazon ECS/EKS: AWS managed container services
- Azure AKS: Microsoft's managed Kubernetes service
- Google GKE: Google's managed Kubernetes service
Consider integrating the following tools:
- Prometheus: Monitoring and alerting
- Grafana: Visualization and dashboards
- ELK Stack: Elasticsearch, Logstash, and Kibana for log management
- Fluentd/Fluent Bit: Log collection and forwarding
Possible causes and solutions:
- Application crashes on startup - check logs with
docker logs [container_id] - Missing entry point - ensure CMD or ENTRYPOINT is properly defined
- Running a non-daemon process - use
-dflag to run in detached mode
Possible causes and solutions:
- Port already in use - change the host port mapping
- Permission issues - try running with sudo or add your user to the docker group
- Incorrect port exposure - ensure EXPOSE directive matches your application's port
Solution:
# Change ownership of host directory to match container user
sudo chown -R 1000:1000 ./data
# Or adjust permissions on host directory
sudo chmod -R 777 ./dataTroubleshooting steps:
- Check network configuration with
docker network inspect - Ensure containers are on the same network
- Try using container names instead of IP addresses
- Verify firewall settings aren't blocking connections







