🚀 Week 8: Capstone Project

Day 2: Application Setup

⏱ 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/ -v

Multi-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-network

Nginx 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/info

Lab 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-test

Lab 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