⏱ Duration: 5 Hours
📚 Learning Objectives
- Build a Python REST API application
- Implement health check and API endpoints
- Write unit tests for the application
- Create an optimized multi-stage Dockerfile
- Set up Docker Compose for local development
📖 Core Concepts (2 Hours)
Python Flask Application
# app/requirements.txt
Flask==3.0.0
gunicorn==21.2.0
pytest==7.4.3
requests==2.31.0# app/main.py
from flask import Flask, jsonify
import os
app = Flask(__name__)
# Configuration from environment variables
APP_VERSION = os.getenv("APP_VERSION", "1.0.0")
ENVIRONMENT = os.getenv("ENVIRONMENT", "development")
@app.route("/")
def home():
"""Root endpoint with welcome message"""
return jsonify({
"message": "Welcome to DevOps Capstone API",
"version": APP_VERSION,
"environment": ENVIRONMENT
})
@app.route("/health")
def health():
"""Health check endpoint for load balancers and monitoring"""
return jsonify({
"status": "healthy",
"version": APP_VERSION
}), 200
@app.route("/api/info")
def info():
"""Application information endpoint"""
return jsonify({
"app_name": "DevOps Capstone Project",
"version": APP_VERSION,
"environment": ENVIRONMENT,
"python_version": "3.11",
"framework": "Flask"
})
@app.route("/api/status")
def status():
"""Detailed status endpoint"""
return jsonify({
"service": "capstone-api",
"status": "running",
"uptime": "healthy",
"dependencies": {
"database": "not_configured",
"cache": "not_configured"
}
})
if __name__ == "__main__":
port = int(os.getenv("PORT", 8000))
debug = os.getenv("FLASK_DEBUG", "False").lower() == "true"
app.run(host="0.0.0.0", port=port, debug=debug)Unit Tests
# tests/test_app.py
import pytest
import sys
import os
# Add app directory to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'app'))
from main import app
@pytest.fixture
def client():
"""Create test client"""
app.config['TESTING'] = True
with app.test_client() as client:
yield client
def test_home_endpoint(client):
"""Test root endpoint returns welcome message"""
response = client.get('/')
assert response.status_code == 200
data = response.get_json()
assert 'message' in data
assert 'DevOps Capstone' in data['message']
def test_health_endpoint(client):
"""Test health check returns healthy status"""
response = client.get('/health')
assert response.status_code == 200
data = response.get_json()
assert data['status'] == 'healthy'
def test_info_endpoint(client):
"""Test info endpoint returns app details"""
response = client.get('/api/info')
assert response.status_code == 200
data = response.get_json()
assert 'app_name' in data
assert 'version' in data
def test_status_endpoint(client):
"""Test status endpoint returns service status"""
response = client.get('/api/status')
assert response.status_code == 200
data = response.get_json()
assert data['status'] == 'running'
# Run tests: pytest tests/ -vMulti-Stage Dockerfile
# docker/Dockerfile
# Stage 1: Builder - Install dependencies
FROM python:3.11-slim as builder
WORKDIR /app
# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Copy and install Python dependencies
COPY app/requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt
# Stage 2: Production - Minimal runtime image
FROM python:3.11-slim as production
# Create non-root user for security
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
WORKDIR /app
# Copy installed packages from builder
COPY --from=builder /root/.local /home/appuser/.local
# Copy application code
COPY app/ .
# Set environment variables
ENV PATH=/home/appuser/.local/bin:$PATH
ENV PYTHONUNBUFFERED=1
ENV PORT=8000
ENV ENVIRONMENT=production
# Change ownership and switch to non-root user
RUN chown -R appuser:appgroup /app
USER appuser
# Expose application port
EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
# Run with gunicorn for production
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "main:app"]Docker Compose for Local Development
# docker/docker-compose.yml
version: '3.8'
services:
app:
build:
context: ..
dockerfile: docker/Dockerfile
container_name: capstone-app
ports:
- "8000:8000"
environment:
- ENVIRONMENT=development
- APP_VERSION=1.0.0
- FLASK_DEBUG=true
- PORT=8000
volumes:
# Mount app code for live reloading in development
- ../app:/app:ro
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
restart: unless-stopped
# Optional: Add nginx reverse proxy
nginx:
image: nginx:alpine
container_name: capstone-nginx
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- app
restart: unless-stopped
# Networks (optional)
networks:
default:
name: capstone-networkNginx Configuration (Optional)
# docker/nginx.conf
events {
worker_connections 1024;
}
http {
upstream app_servers {
server app:8000;
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://app_servers;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 30s;
proxy_read_timeout 30s;
}
location /health {
proxy_pass http://app_servers/health;
access_log off;
}
}
}🔬 Hands-on Lab (2.5 Hours)
Lab 1: Create the Application
# Navigate to project directory
cd capstone-project
# Create requirements.txt
cat > app/requirements.txt << 'EOF'
Flask==3.0.0
gunicorn==21.2.0
pytest==7.4.3
requests==2.31.0
EOF
# Create main.py (copy the Flask app code from above)
cat > app/main.py << 'EOF'
from flask import Flask, jsonify
import os
app = Flask(__name__)
APP_VERSION = os.getenv("APP_VERSION", "1.0.0")
ENVIRONMENT = os.getenv("ENVIRONMENT", "development")
@app.route("/")
def home():
return jsonify({
"message": "Welcome to DevOps Capstone API",
"version": APP_VERSION,
"environment": ENVIRONMENT
})
@app.route("/health")
def health():
return jsonify({"status": "healthy", "version": APP_VERSION}), 200
@app.route("/api/info")
def info():
return jsonify({
"app_name": "DevOps Capstone Project",
"version": APP_VERSION,
"environment": ENVIRONMENT,
"framework": "Flask"
})
if __name__ == "__main__":
port = int(os.getenv("PORT", 8000))
app.run(host="0.0.0.0", port=port, debug=True)
EOF
# Test locally (optional - requires Python installed)
cd app
pip install -r requirements.txt
python main.py &
curl http://localhost:8000/health
curl http://localhost:8000/api/infoLab 2: Create and Test Docker Image
# Create Dockerfile
cat > docker/Dockerfile << 'EOF'
FROM python:3.11-slim as builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends gcc && rm -rf /var/lib/apt/lists/*
COPY app/requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt
FROM python:3.11-slim as production
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
WORKDIR /app
COPY --from=builder /root/.local /home/appuser/.local
COPY app/ .
ENV PATH=/home/appuser/.local/bin:$PATH
ENV PYTHONUNBUFFERED=1
ENV PORT=8000
RUN chown -R appuser:appgroup /app
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "main:app"]
EOF
# Build Docker image
docker build -t capstone-app:latest -f docker/Dockerfile .
# Run container
docker run -d -p 8000:8000 --name capstone-test capstone-app:latest
# Test endpoints
curl http://localhost:8000/
curl http://localhost:8000/health
curl http://localhost:8000/api/info
# Check container logs
docker logs capstone-test
# Check image size (should be optimized)
docker images capstone-app:latest
# Clean up
docker stop capstone-test && docker rm capstone-testLab 3: Run Tests and Push to Registry
# Create test file
cat > tests/test_app.py << 'EOF'
import pytest
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'app'))
from main import app
@pytest.fixture
def client():
app.config['TESTING'] = True
with app.test_client() as client:
yield client
def test_home(client):
response = client.get('/')
assert response.status_code == 200
def test_health(client):
response = client.get('/health')
assert response.status_code == 200
assert response.get_json()['status'] == 'healthy'
def test_info(client):
response = client.get('/api/info')
assert response.status_code == 200
EOF
# Run tests
cd capstone-project
pip install pytest
pytest tests/ -v
# Tag and push to Docker Hub
docker tag capstone-app:latest YOUR_DOCKERHUB_USERNAME/capstone-app:latest
docker tag capstone-app:latest YOUR_DOCKERHUB_USERNAME/capstone-app:v1.0.0
# Login to Docker Hub
docker login
# Push images
docker push YOUR_DOCKERHUB_USERNAME/capstone-app:latest
docker push YOUR_DOCKERHUB_USERNAME/capstone-app:v1.0.0
# Verify on Docker Hub
echo "Image pushed to: https://hub.docker.com/r/YOUR_DOCKERHUB_USERNAME/capstone-app"Lab 4: Docker Compose Development Setup
# Create docker-compose.yml
cat > docker/docker-compose.yml << 'EOF'
version: '3.8'
services:
app:
build:
context: ..
dockerfile: docker/Dockerfile
container_name: capstone-app
ports:
- "8000:8000"
environment:
- ENVIRONMENT=development
- APP_VERSION=1.0.0
restart: unless-stopped
EOF
# Start with Docker Compose
cd docker
docker-compose up -d
# Check status
docker-compose ps
# View logs
docker-compose logs -f app
# Test application
curl http://localhost:8000/health
# Stop services
docker-compose down✅ Day 2 Checklist
- Created Python Flask REST API application
- Implemented health check and info endpoints
- Wrote unit tests with pytest
- Created optimized multi-stage Dockerfile
- Built and tested Docker image locally
- Set up Docker Compose for development
- Pushed image to Docker Hub