Python course Β· Module 12: Final Project

Deployment to Production

6 min read
In this lesson4

"It works on my machine" is the most common sentence in the history of programming. Your laptop has the right Python version, the installed packages and the environment variables. A cloud server has none of these. Deployment is the moment when code becomes a product, and our job is to pack the camp so that it unfolds identically anywhere on the savanna.

Docker - Containerization

Docker packs the application together with the system, Python and dependencies into an image. A container is a running image. We will use a multi-stage build: the first stage builds the packages, the second takes only the result, which makes the final image smaller. The first stage:

1# Dockerfile
2FROM python:3.12-slim AS builder
3
4WORKDIR /app
5COPY requirements.txt .
6RUN pip wheel --no-cache-dir --no-deps --wheel-dir /wheels -r requirements.txt

pip wheel turns every dependency into a ready .whl file. The --no-deps flag means pip does not pull in transitive dependencies, so requirements.txt must contain the full, pinned list (for example from pip freeze). The second stage:

1FROM python:3.12-slim
2
3WORKDIR /app
4
5# Dependencies
6COPY --from=builder /wheels /wheels
7RUN pip install --no-cache-dir /wheels/*
8
9# Security
10RUN useradd -m -u 1000 appuser
11USER appuser
12
13# Application
14COPY --chown=appuser:appuser . .
15
16EXPOSE 8000
17CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

The order matters. We install the packages while still root, and only then switch to the regular user appuser, because the application should not run with root privileges. If we switched users before pip install, pip would install the packages into the home directory and the uvicorn command would not be on the path. You build the image with docker build -t app:latest . (the dot is the directory with the Dockerfile) and run it locally with docker run -p 8000:8000 app:latest. The typical order of work: Dockerfile, build, local test, push to a registry, deploy to a server.

Docker Compose manages several containers as one service. Our application needs Qdrant and Redis, and we describe everything in a single file. First, the application service:

1# docker-compose.yml
2services:
3  app:
4    build: .
5    ports:
6      - "8000:8000"
7    environment:
8      - OPENAI_API_KEY=${OPENAI_API_KEY}
9      - QDRANT_URL=http://qdrant:6333
10      - REDIS_URL=redis://redis:6379
11    depends_on:
12      - qdrant
13      - redis
14    healthcheck:
15      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
16      interval: 30s
17      timeout: 10s
18      retries: 3

depends_on sets the start order, and healthcheck calls the /health endpoint every 30 seconds. We use Python from the standard library for that, because the python:3.12-slim image does not include curl. The version field at the top of the file is obsolete, current Docker Compose ignores it, so we do not write it. Next, the databases and volumes:

1  qdrant:
2    image: qdrant/qdrant:latest
3    volumes:
4      - qdrant_data:/qdrant/storage
5    ports:
6      - "6333:6333"
7
8  redis:
9    image: redis:alpine
10    volumes:
11      - redis_data:/data
12
13volumes:
14  qdrant_data:
15  redis_data:

Volumes make the data survive a container restart. The latest tag is convenient to start with, but in production pin a specific image version so that an update does not arrive unexpectedly.

GitHub Actions - CI/CD

CI/CD stands for Continuous Integration and Continuous Deployment: every push automatically goes through testing, building and deployment. The pipeline has consecutive stages: test, build, push to the registry, deploy. The first stage runs the tests:

1# .github/workflows/deploy.yml
2name: Deploy
3
4on:
5  push:
6    branches: [main]
7
8jobs:
9  test:
10    runs-on: ubuntu-latest
11    steps:
12      - uses: actions/checkout@v4
13      - uses: actions/setup-python@v5
14        with:
15          python-version: "3.12"
16      - run: pip install -r requirements.txt
17      - run: pytest tests/ -v
18

If the tests fail, the later stages do not run. The second stage builds the image and pushes it to Docker Hub (needs: test means "only after successful tests"):

1  build:
2    needs: test
3    runs-on: ubuntu-latest
4    steps:
5      - uses: actions/checkout@v4
6
7      - name: Build Docker image
8        run: docker build -t app:latest .
9
10      - name: Push to Registry
11        run: |
12          echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
13          docker tag app:latest ${{ secrets.DOCKER_USERNAME }}/ai-assistant:latest
14          docker push ${{ secrets.DOCKER_USERNAME }}/ai-assistant:latest
15

The password reaches docker login through --password-stdin, so it does not show up in the logs. The last stage connects to the server over SSH and replaces the containers:

1  deploy:
2    needs: build
3    runs-on: ubuntu-latest
4    steps:
5      - name: Deploy to server
6        uses: appleboy/ssh-action@v1
7        with:
8          host: ${{ secrets.SERVER_HOST }}
9          username: ${{ secrets.SERVER_USER }}
10          key: ${{ secrets.SSH_KEY }}
11          script: |
12            cd /app
13            docker compose pull
14            docker compose up -d

We pin the action to a version (@v1) rather than to the master branch, because a branch can change without warning. That matters especially when the action receives the SSH key to your server.

Cloud Deployment Options

Where do you run the container? Here are the four main routes with their pros and cons:

1"""
2Deployment options:
3
41. VPS (DigitalOcean, Hetzner)
5   - Full control
6   - Low cost (depends on the provider and machine size)
7   - Requires management
8
92. Platform as a Service
10   - Railway, Render, Fly.io
11   - Easy deployment
12   - Auto-scaling
13
143. Kubernetes
15   - For large systems
16   - High availability
17   - Complex configuration
18
194. Serverless
20   - AWS Lambda, Google Cloud Run
21   - Pay-per-use
22   - Cold starts
23"""

Prices change often, so compare them in the providers' current price lists. My advice for a first portfolio project: a PaaS platform, because you deploy a container in a few minutes and do not have to administer a server.

Monitoring

After deployment you need to know whether the application is alive and how fast it responds. The prometheus_client library provides counters (Counter) and timing histograms (Histogram), which Prometheus reads from the /metrics endpoint. app is the FastAPI application from the previous lesson:

1# Prometheus metrics
2from prometheus_client import CONTENT_TYPE_LATEST, Counter, Histogram, generate_latest
3from fastapi import Response
4
5REQUEST_COUNT = Counter('requests_total', 'Total requests', ['method', 'endpoint'])
6REQUEST_LATENCY = Histogram('request_latency_seconds', 'Request latency')
7
8@app.middleware("http")
9async def metrics_middleware(request, call_next):
10    REQUEST_COUNT.labels(request.method, request.url.path).inc()
11    with REQUEST_LATENCY.time():
12        response = await call_next(request)
13    return response
14
15@app.get("/metrics")
16async def metrics():
17    return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)

The middleware counts every request and measures its duration. CONTENT_TYPE_LATEST is the correct content type for the Prometheus format, safer than a hand-typed text/plain. Be careful with the path label: with URLs containing identifiers, the number of series can grow without limit.

Your project is ready for production! In the next lesson we will build a portfolio that shows it to the world.

Remember: a well-packed camp unfolds identically anywhere on the savanna, and that is exactly what a container gives you.

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What is the advantage of Docker multi-stage build?

  2. 2. What does Docker Compose allow you to do?

These are 2 of 3 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Click in order

    Click the elements to build a deployment pipeline in the correct order:

  • Vertical ordering

    Arrange the steps of working with Docker in the correct order:

  • Code editor

    Write a Dockerfile for a Python AI application

  • Horizontal ordering

    Arrange the Docker build command with tag in the correct order:

  • Click in order

    Click the elements to build a docker run command:

  • Code editor

    Create a docker-compose.yml file for an AI application with a database

  • Vertical ordering

    Arrange the stages of a CI/CD pipeline in the correct order:

Useful articles