Kurs Python · Moduł 12: Projekt końcowy
Deployment do Produkcji
W tej lekcji4
"U mnie działa" to najczęstsze zdanie w historii programowania. Na Twoim laptopie jest właściwa wersja Pythona, zainstalowane pakiety i ustawione zmienne środowiskowe. Serwer w chmurze nie ma nic z tych rzeczy. Deployment to moment, gdy kod staje się produktem, a naszym zadaniem jest spakować obóz tak, by rozłożył się identycznie w każdym miejscu sawanny.
Docker - konteneryzacja
Docker pakuje aplikację razem z systemem, Pythonem i zależnościami w obraz. Kontener to uruchomiony obraz. Użyjemy multi-stage build: pierwszy etap buduje paczki, drugi zabiera tylko wynik, dzięki czemu finalny obraz jest mniejszy. Etap pierwszy:
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.txtpip wheel zamienia każdą zależność w gotowy plik .whl. Flaga --no-deps oznacza, że pip nie dociąga zależności pośrednich, więc requirements.txt musi zawierać pełną, przypiętą listę (na przykład z pip freeze). Etap drugi:
1FROM python:3.12-slim
2
3WORKDIR /app
4
5# Dependencies
6COPY /wheels /wheels
7RUN pip install --no-cache-dir /wheels/*
8
9# Security
10RUN useradd -m -u 1000 appuser
11USER appuser
12
13# Application
14COPY . .
15
16EXPOSE 8000
17CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]Kolejność ma znaczenie. Pakiety instalujemy jeszcze jako root, a dopiero potem przełączamy się na zwykłego użytkownika appuser, bo aplikacja nie powinna działać z uprawnieniami roota. Gdybyśmy zmienili użytkownika przed pip install, pip zainstalowałby pakiety do katalogu domowego i polecenie uvicorn nie byłoby na ścieżce. Obraz budujesz poleceniem docker build -t app:latest . (kropka to katalog z Dockerfile), a lokalnie uruchamiasz przez docker run -p 8000:8000 app:latest. Typowa kolejność pracy: Dockerfile, build, test lokalny, push do registry, wdrożenie na serwer.
Docker Compose zarządza kilkoma kontenerami jako jedną usługą. Nasza aplikacja potrzebuje Qdranta i Redisa, a wszystko opisujemy w jednym pliku. Najpierw serwis aplikacji:
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: 3depends_on ustala kolejność startu, a healthcheck co 30 sekund pyta endpoint /health. Używamy do tego Pythona z biblioteki standardowej, bo obraz python:3.12-slim nie zawiera curl. Pole version na początku pliku jest przestarzałe, obecny Docker Compose je ignoruje, więc go nie piszemy. Dalej bazy danych i wolumeny:
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:Wolumeny sprawiają, że dane przetrwają restart kontenera. Tag latest jest wygodny na start, ale w produkcji przypnij konkretną wersję obrazu, żeby aktualizacja nie przyszła niespodziewanie.
GitHub Actions - CI/CD
CI/CD to Continuous Integration i Continuous Deployment: każdy push automatycznie przechodzi przez testy, budowanie i wdrożenie. Pipeline ma kolejne etapy: test, build, push do registry, deploy. Pierwszy etap uruchamia testy:
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
18Jeśli testy nie przejdą, dalsze etapy się nie wykonają. Drugi etap buduje obraz i wysyła go do Docker Huba (needs: test oznacza "tylko po udanych testach"):
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
15Hasło trafia do docker login przez --password-stdin, więc nie widać go w logach. Ostatni etap łączy się z serwerem przez SSH i podmienia kontenery:
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 -dAkcję przypinamy do wersji (@v1), a nie do gałęzi master, bo gałąź może się zmienić bez ostrzeżenia. To ważne szczególnie wtedy, gdy akcja dostaje klucz SSH do Twojego serwera.
Cloud Deployment Options
Gdzie postawić kontener? Oto cztery główne drogi z ich wadami i zaletami:
1"""
2Opcje deploymentu:
3
41. VPS (DigitalOcean, Hetzner)
5 - Pełna kontrola
6 - Niski koszt (zależny od dostawcy i rozmiaru maszyny)
7 - Wymaga zarządzania
8
92. Platform as a Service
10 - Railway, Render, Fly.io
11 - Łatwy deployment
12 - Auto-scaling
13
143. Kubernetes
15 - Dla dużych systemów
16 - Wysoka dostępność
17 - Złożona konfiguracja
18
194. Serverless
20 - AWS Lambda, Google Cloud Run
21 - Pay-per-use
22 - Cold starts
23"""Ceny zmieniają się często, więc porównuj je w aktualnych cennikach dostawców. Moja rada na pierwszy projekt portfolio: platforma PaaS, bo wdrożysz kontener w kilka minut i nie musisz administrować serwerem.
Monitoring
Po wdrożeniu musisz wiedzieć, czy aplikacja żyje i jak szybko odpowiada. Biblioteka prometheus_client udostępnia liczniki (Counter) i histogramy czasu (Histogram), które Prometheus odczytuje z endpointu /metrics. app to aplikacja FastAPI z poprzedniej lekcji:
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)Middleware liczy każde żądanie i mierzy jego czas. CONTENT_TYPE_LATEST to właściwy typ treści formatu Prometheusa, bezpieczniejszy niż ręcznie wpisane text/plain. Uważaj na etykietę ze ścieżką: przy adresach z identyfikatorami liczba serii może rosnąć bez końca.
Twój projekt jest gotowy do produkcji! W następnej lekcji zbudujemy portfolio, które pokaże go światu.
Pamiętaj: dobrze spakowany obóz rozkłada się identycznie w każdym miejscu sawanny, i to właśnie daje Ci kontener.
Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Po co używamy multi-stage build w Dockerfile?
2. Do czego służy Docker Compose?
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Klikanie w kolejności
Kliknij w kolejności:
- Układanie w pionie
Uporządkuj kroki:
- Edytor kodu
Stwórz produkcyjny Dockerfile
- Układanie w poziomie
Ułóż elementy:
- Klikanie w kolejności
Kliknij w kolejności:
- Edytor kodu
Stwórz docker-compose z serwisami: app (FastAPI) i db (MongoDB).
- Układanie w pionie
Ułóż etapy CI/CD pipeline: