commit 9b2db18fa3d957575d2f278826eba40eb5e5628f Author: stephan Date: Thu Dec 4 11:54:41 2025 +0100 first commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e679492 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +**/.git +**/.gitignore +**/.dockerignore +**/Dockerfile +**/Makefile +**/docker-compose.yml +**/README.md +**/__pycache__ +**/.venv +**/.env +**/*.pyc +**/*.pyo +**/*.pyd +guide.md \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..745323f --- /dev/null +++ b/.gitignore @@ -0,0 +1,112 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.pyc +*.pyo +*.pyd + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other info into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# celery +celerybeat-schedule +celerybeat.pid + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak +venv.bak + +# IDE / Editor specific +.vscode/ +.idea/ +*.swp +*.swo + +# Docker +.docker/ +docker-compose.override.yml# AI-specific exclusions +.claude/ +.gemini/ +.geminiignore +*.log diff --git a/DesktopPopOS b/DesktopPopOS new file mode 100644 index 0000000..57f7a98 --- /dev/null +++ b/DesktopPopOS @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACD2Ngm3v/8gcw1+KQWA2RS8KW14lWN3WlFVQWOnkxt7ggAAAJhedgRjXnYE +YwAAAAtzc2gtZWQyNTUxOQAAACD2Ngm3v/8gcw1+KQWA2RS8KW14lWN3WlFVQWOnkxt7gg +AAAEDWLbOUG/mRh4cfxs16DNf01/f6MRWk5wAhyXTPMn98VPY2Cbe//yBzDX4pBYDZFLwp +bXiVY3daUVVBY6eTG3uCAAAAEXMud2lsa2Vuc0BnbXgubmV0AQIDBA== +-----END OPENSSH PRIVATE KEY----- diff --git a/DesktopPopOS.pub b/DesktopPopOS.pub new file mode 100644 index 0000000..7bd7dd6 --- /dev/null +++ b/DesktopPopOS.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPY2Cbe//yBzDX4pBYDZFLwpbXiVY3daUVVBY6eTG3uC s.wilkens@gmx.net diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..632a678 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +# --- Builder Stage --- +# This stage installs all Python dependencies into a virtual environment. +FROM python:3.11 as builder + +WORKDIR /opt/venv + +# Create a virtual environment +RUN python -m venv . + +# Activate the virtual environment and install dependencies +COPY requirements.txt . +RUN . /opt/venv/bin/activate && pip install --no-cache-dir -r requirements.txt + + +# --- Runner Stage --- +# This stage creates the final, lean image. +FROM python:3.11-slim + +# Create a non-privileged user for security +RUN useradd --create-home --shell /bin/bash appuser + +WORKDIR /home/appuser/app + +# Copy the virtual environment from the builder stage +COPY --from=builder /opt/venv /opt/venv + +# Copy the application code +COPY app/ . + +# Set the PATH to include the venv binaries +ENV PATH="/opt/venv/bin:$PATH" + +# Expose the port the app runs on +EXPOSE 8000 + +# Switch to the non-privileged user +USER appuser + +# Command to run the application using Gunicorn +# This is a production-ready WSGI server. +CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "-w", "2", "-b", "0.0.0.0:8000", "main:app"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4d1499d --- /dev/null +++ b/Makefile @@ -0,0 +1,131 @@ +# Makefile for managing the Dockerized Python application + +# --- Configuration --- +# Use the current directory name as the default image name +IMAGE_NAME := $(shell basename $(CURDIR)) +# Default tag +TAG := latest +# Placeholder for a remote registry (e.g., your Docker Hub username or Gitea instance) +# For Gitea: REGISTRY ?= your-gitea-host:port/owner +# Example: REGISTRY ?= localhost:3000/myuser +REGISTRY ?= your-registry-username + +# Default internal application port (exposed to Traefik) +PORT := 8000 + + +# Function to find next free host port for the app and ask user +# This function will echo the chosen port if successful, or exit with an error. +define check_app_port_free + @local start_port=$(PORT); \ + local found_port=$$start_port; \ + local is_free=false; \ + \ + if ! ss -tulnp | grep ":$$start_port " > /dev/null; then \ + echo "$$start_port"; \ + exit 0; \ + fi; \ + \ + echo "Port $$start_port is busy. Searching for a free port for the app..."; >&2; \ + while ! $$is_free; do \ + if ! ss -tulnp | grep ":$$found_port " > /dev/null; then \ + is_free=true; \ + else \ + ((found_port++)); \ + if [ "$$found_port" -gt 65535 ]; then \ + echo "ERROR: No free ports found up to 65535. Aborting." >&2; \ + exit 1; \ + fi; \ + fi; \ + done; \ + \ + echo "Port $$start_port is busy. I found port $$found_port to be free for the app." >&2; \ + read -p "Do you want to use port $$found_port for the app? (y/N): " choice; \ + case "$$choice" in \ + y|Y ) \ + echo "$$found_port"; \ + ;; \ + * ) \ + echo "Operation cancelled by user." >&2; \ + exit 1; \ + ;; \ + esac; +endef + +# --- Docker Commands --- + +.PHONY: build +build: + @echo "Building Docker image: $(IMAGE_NAME):$(TAG)" + docker build -t $(IMAGE_NAME):$(TAG) . + +.PHONY: run +run: + @export SELECTED_HOST_PORT=$(shell bash -c 'func() { $(check_app_port_free) }; func') && \ + echo "Using host port $$SELECTED_HOST_PORT for single app container" && \ + docker run -d -p $$SELECTED_HOST_PORT:$(PORT) --name $(IMAGE_NAME) $(IMAGE_NAME):$(TAG) + +.PHONY: stop +stop: + @echo "Stopping Docker container: $(IMAGE_NAME)" + docker stop $(IMAGE_NAME) || true + docker rm $(IMAGE_NAME) || true + +.PHONY: logs +logs: + @echo "Showing logs for container: $(IMAGE_NAME)" + docker logs -f $(IMAGE_NAME) + +.PHONY: shell +shell: + @echo "Accessing shell in container: $(IMAGE_NAME)" + docker exec -it $(IMAGE_NAME) /bin/bash + +# --- Docker Compose Commands --- + +.PHONY: up +up: + @echo "Starting development environment with Docker Compose (integrating with external Traefik)..." + docker-compose up --build -d + +.PHONY: down +down: + @echo "Stopping development environment with Docker Compose..." + docker-compose down + +# --- Image Management --- + +.PHONY: tag +tag: + @echo "Tagging image $(IMAGE_NAME):$(TAG) as $(REGISTRY)/$(IMAGE_NAME):$(TAG)" + docker tag $(IMAGE_NAME):$(TAG) $(REGISTRY)/$(IMAGE_NAME):$(TAG) + +.PHONY: push +push: tag + @echo "Pushing image $(REGISTRY)/$(IMAGE_NAME):$(TAG) to registry..." + docker push $(REGISTRY)/$(IMAGE_NAME):$(TAG) + +# --- Cleanup --- + +.PHONY: clean +clean: + @echo "Cleaning up stopped containers and dangling images..." + docker container prune -f + docker image prune -f + +.PHONY: help +help: + @echo "Available commands:" + @echo " build - Build the Docker image" + @echo " run - Run the Docker container" + @echo " stop - Stop and remove the Docker container" + @echo " logs - Follow the logs of the container" + @echo " shell - Get a shell inside the running container" + @echo " up - Start the dev environment with docker-compose (integrates with external Traefik)" + @echo " down - Stop the dev environment with docker-compose" + @echo " tag - Tag the image for a registry" + @echo " push - Push the image to a registry (after tagging)" + @echo " clean - Clean up unused containers and images" + @echo " help - Show this help message" + +.DEFAULT_GOAL := help diff --git a/README.md b/README.md new file mode 100644 index 0000000..a4d4337 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# ✨ Python Backend Containerization Guide ✨ + +## 🚀 Starte deine Python-Anwendung mit Docker! + +Dieses Repository bietet dir eine **umfassende und praktische Anleitung**, um deine Python-Backend-Anwendungen effizient und professionell mit Docker zu containerisieren. Egal, ob du eine schnelle Entwicklungsumgebung oder einen robusten Produktions-Workflow suchst – hier bist du richtig! + +--- + +### 🎉 Schnellstart – Deine App in wenigen Sekunden online! + +Befolge diese Schritte, um die Beispiel-FastAPI-Anwendung sofort zum Laufen zu bringen: + +1. **Stelle sicher, dass Docker läuft.** + *(Prüfe, ob Docker Desktop oder der Docker Daemon aktiv ist.)* + +2. **Stelle sicher, dass Traefik läuft und das 'web'-Netzwerk bereitstellt.** + *(Die App integriert sich in eine **bestehende** Traefik-Instanz. Eine Beispiel-`docker-compose-traefik.yml` findest du in `guide.md`.)* + +3. **Navigiere ins Projektverzeichnis:** + ```bash + cd python-container-guide + ``` + +4. **Starte die Entwicklungsumgebung (integriert sich in deinen Traefik-Router):** + ```bash + make up + ``` + *💡 Dieser Befehl startet deine FastAPI-Anwendung und verbindet sie mit dem 'web'-Netzwerk deines laufenden Traefik. Die Portprüfung für Traefik selbst musst du extern vornehmen, da diese App Traefik nicht startet.* + +5. **Greife auf deine Anwendung zu:** + Öffne deinen Webbrowser und besuche: `http://localhost` + *(Dein externer Traefik sollte die Anfrage automatisch zur FastAPI-Anwendung weiterleiten.)* + +6. **Beende die Entwicklungsumgebung:** + ```bash + make down + ``` + +--- + +### 🌟 Was du hier findest: + +* **FastAPI Beispiel:** Eine einfache, aber vollständige Python-Webanwendung. +* **Optimales `Dockerfile`:** Für schnelle, kleine und sichere Docker-Images dank Multi-Stage-Builds. +* **`docker-compose.yml`:** Konfiguriert deine lokale Entwicklungsumgebung, um sich in einen **externen Traefik** zu integrieren und Hot-Reloading zu ermöglichen. +* **Integration in Traefik:** Zeigt, wie deine App sich in einen bestehenden Traefik Reverse Proxy einklinkt – ideal für Microservices und eine produktionsnahe lokale Umgebung. +* **`Makefile`:** Vereinfacht alle Docker-Befehle (`build`, `run`, `stop`, `push`) zu intuitiven Kommandos. +* **Interaktive Port-Wahl:** Bei Konflikten schlägt das System freie Ports vor – nie wieder Port-Chaos! (Besonders nützlich für `make run`, wenn du die App *ohne Traefik* starten willst). +* **Best Practices:** Sorgfältig konfigurierte `.gitignore` und `.dockerignore` für saubere Repositories und schlanke Images. +* **Umfassende Dokumentation:** Eine detaillierte `guide.md` erklärt jedes Konzept im Detail. + +--- + +### 📚 Tauche tiefer ein: + +Für alle Details, Best Practices und fortgeschrittene Themen empfehlen wir dir unsere Guides: + +* **Der Haupt-Leitfaden:** `guide.md` – Hier findest du alles rund um die Containerisierung deiner Python-Anwendung. +* **Gitea Container Registry Guide:** `gitea-registry-guide.md` – Erfahre, wie du Gitea als deine private Docker Container Registry einrichtest und nutzt. + +Viel Erfolg beim Containerisieren! \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..5103e5d --- /dev/null +++ b/app/main.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI + +app = FastAPI() + +@app.get("/") +def read_root(): + return {"message": "Hello World"} + +@app.get("/health") +def health_check(): + return {"status": "ok"} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..83e4fce --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +version: '3.8' + +services: + app: + build: . + volumes: + - ./app:/home/appuser/app + command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] + labels: + - "traefik.enable=true" + # Define a router for the app service + - "traefik.http.routers.app-router.rule=Host(`localhost`)" + - "traefik.http.routers.app-router.entrypoints=web" + # Define the service port (internal to the container) + - "traefik.http.services.app-service.loadbalancer.server.port=8000" + networks: + - web + +networks: + web: + external: true # This network is assumed to be created by a central Traefik instance diff --git a/gitea-registry-guide.md b/gitea-registry-guide.md new file mode 100644 index 0000000..403bd6e --- /dev/null +++ b/gitea-registry-guide.md @@ -0,0 +1,166 @@ +# Anleitung: Gitea als Container Registry bereitstellen und konfigurieren + +Diese Anleitung beschreibt, wie Sie eine Gitea-Instanz bereitstellen und so konfigurieren, dass sie auch als private Docker Container Registry funktioniert. Dies ist besonders nützlich für Teams oder Projekte, die eine integrierte Lösung für Code- und Image-Verwaltung suchen. + +## 1. Einleitung + +Gitea ist ein leichtgewichtiger, selbst gehosteter Git-Dienst, der auch Funktionen wie Issue Tracking, Code-Review und seit Version 1.12 eine integrierte Container Registry bietet. Die Verwendung einer Gitea-Registry ermöglicht es Ihnen, Ihre Docker-Images direkt neben Ihrem Quellcode zu hosten und zu verwalten. + +## 2. Voraussetzungen + +* **Docker und Docker Compose:** Für die einfache Bereitstellung von Gitea. +* **Domainname oder IP-Adresse:** Die unter der Gitea erreichbar sein soll. +* **Offener Port 3000 (HTTP) und optional 22 (SSH) und 443 (HTTPS) auf dem Host-System.** + +## 3. Gitea mit Docker Compose bereitstellen + +Wir verwenden Docker Compose, um Gitea zusammen mit einer PostgreSQL-Datenbank bereitzustellen. Erstellen Sie eine `docker-compose.yml` in einem neuen Verzeichnis (z.B. `gitea-setup`): + +```yaml +version: "3" + +services: + gitea: + image: gitea/gitea:1.21.11 # Aktuelle stabile Version verwenden + container_name: gitea + environment: + - USER_UID=1000 + - USER_GID=1000 + - GITEA__database__DB_TYPE=postgres + - GITEA__database__HOST=db:5432 + - GITEA__database__NAME=gitea + - GITEA__database__USER=gitea + - GITEA__database__PASS=gitea_password # Starkes Passwort wählen! + - GITEA__server__ROOT_URL=http://your_gitea_domain_or_ip:3000/ # Wichtig: Anpassen! + - GITEA__server__SSH_DOMAIN=your_gitea_domain_or_ip # Optional, wenn SSH genutzt wird + - GITEA__server__DISABLE_SSH=false # Optional, wenn SSH genutzt wird + - GITEA__container_registry__ENABLED=true # Container Registry aktivieren + - GITEA__container_registry__MAX_TAGS_PER_REPOSITORY=10 # Optional: Anzahl Tags pro Repo begrenzen + restart: always + ports: + - "3000:3000" # Gitea HTTP Port + - "2222:22" # Gitea SSH Port (intern 22, extern 2222) + volumes: + - ./gitea_data:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + depends_on: + - db + networks: + - gitea_network + + db: + image: postgres:15 + container_name: gitea_db + restart: always + environment: + - POSTGRES_USER=gitea + - POSTGRES_PASSWORD=gitea_password # Muss mit GITEA__database__PASS übereinstimmen + - POSTGRES_DB=gitea + volumes: + - ./postgres_data:/var/lib/postgresql/data + networks: + - gitea_network + +networks: + gitea_network: + driver: bridge +``` + +**Erste Schritte nach dem Start:** + +1. Speichern Sie die Konfiguration als `docker-compose.yml`. +2. Starten Sie die Dienste im Verzeichnis der Datei: `docker-compose up -d`. +3. Öffnen Sie einen Browser und navigieren Sie zu `http://your_gitea_domain_or_ip:3000/`. +4. Folgen Sie dem Initialisierungsassistenten. Achten Sie darauf, dass die Datenbankeinstellungen korrekt sind (sollten aus den Umgebungsvariablen übernommen werden). +5. Erstellen Sie den ersten Administrator-Benutzer. + +## 4. Container Registry Konfiguration (nachträglich) + +Obwohl wir `GITEA__container_registry__ENABLED=true` in der `docker-compose.yml` gesetzt haben, kann es sein, dass die Registry nachträglich aktiviert oder angepasst werden muss, oder Sie Gitea ohne diese Einstellung bereitgestellt haben. + +1. **Gitea-Konfigurationsdatei finden:** + Die Konfigurationsdatei von Gitea ist `app.ini`. Sie befindet sich normalerweise im `gitea_data`-Volume, das wir gemountet haben, unter `/data/gitea/conf/app.ini` *innerhalb des Gitea-Containers*. + + Um die Datei zu bearbeiten, können Sie entweder: + * Den Gitea-Container stoppen, die Datei im gemounteten Volume (z.B. `./gitea_data/gitea/conf/app.ini`) auf Ihrem Host bearbeiten und Gitea neu starten. + * Über `docker exec` in den Container gehen und die Datei mit einem Editor wie `vi` oder `nano` (falls installiert) bearbeiten. Dies ist oft komplizierter. + +2. **`app.ini` bearbeiten:** + Suchen Sie in der `app.ini` den Abschnitt `[container_registry]`. Falls er nicht existiert, fügen Sie ihn hinzu. Stellen Sie sicher, dass `ENABLED = true` gesetzt ist: + + ```ini + [container_registry] + ENABLED = true + # MAX_TAGS_PER_REPOSITORY = 10 # Optional: Begrenzt die Anzahl der Tags pro Repository + ``` + Weitere Optionen können hier konfiguriert werden, z.B. Speicherlimits oder Zugriffsrechte. + +3. **Gitea neu starten:** + Nach Änderungen an der `app.ini` müssen Sie den Gitea-Container neu starten, damit die Änderungen wirksam werden: + ```bash + docker-compose restart gitea + ``` + +## 5. Nutzung der Gitea Container Registry + +Sobald die Registry aktiviert ist, können Sie sie wie jede andere Docker Registry verwenden. + +**Authentifizierung:** + +Bevor Sie Images pushen oder pullen können, müssen Sie sich bei Ihrer Gitea Registry anmelden. Verwenden Sie dafür Ihre Gitea-Benutzerdaten: + +```bash +docker login your_gitea_domain_or_ip:3000 +``` +Ersetzen Sie `your_gitea_domain_or_ip:3000` durch die tatsächliche Adresse und Port Ihrer Gitea-Instanz. + +**Image Tagging:** + +Images müssen nach folgendem Muster getaggt werden: +`your_gitea_domain_or_ip:3000/benutzername/repository-name:tag` + +* `your_gitea_domain_or_ip:3000`: Die Adresse Ihrer Gitea-Instanz. +* `benutzername`: Der Gitea-Benutzer oder die Organisation, unter der das Repository liegt. +* `repository-name`: Der Name des Gitea-Repositories, das mit dem Image verknüpft werden soll. Gitea erstellt automatisch ein Container-Image-Repository, wenn Sie das erste Image pushen. +* `tag`: Ein beliebiges Tag (z.B. `latest`, `v1.0.0`, ein Commit-Hash). + +**Beispiel:** + +Angenommen, Ihre Gitea ist unter `mygitea.com:3000` erreichbar, Ihr Benutzername ist `devuser`, und Sie haben ein Git-Repository namens `my-python-app`. + +1. **Image bauen (wie in unserem Haupt-Leitfaden beschrieben):** + ```bash + make build # Baut my-python-app:latest + ``` + +2. **Image taggen für Gitea:** + ```bash + docker tag my-python-app:latest mygitea.com:3000/devuser/my-python-app:latest + ``` + Oder über den `make`-Befehl aus unserem Haupt-Leitfaden: + ```bash + make tag REGISTRY=mygitea.com:3000/devuser + ``` + +3. **Image pushen:** + ```bash + docker push mygitea.com:3000/devuser/my-python-app:latest + ``` + Oder über den `make`-Befehl aus unserem Haupt-Leitfaden: + ```bash + make push REGISTRY=mygitea.com:3000/devuser + ``` + +4. **Image pullen:** + ```bash + docker pull mygitea.com:3000/devuser/my-python-app:latest + ``` + +## 6. Sicherheitsaspekte + +* **HTTPS:** Für eine produktive Gitea-Instanz im Netzwerk wird dringend empfohlen, HTTPS zu konfigurieren. Dies schützt Ihre Anmeldeinformationen und Image-Daten während der Übertragung. Traefik kann hierfür auch verwendet werden, um HTTPS für Gitea zu terminieren. +* **Zugriffsrechte:** Verwalten Sie die Berechtigungen für Container-Image-Repositories in Gitea wie für Git-Repositories. + +--- +Dieser Leitfaden sollte Ihnen helfen, Gitea erfolgreich als Ihre private Container Registry zu nutzen. diff --git a/guide.md b/guide.md new file mode 100644 index 0000000..cf0ca5d --- /dev/null +++ b/guide.md @@ -0,0 +1,300 @@ +# Leitfaden: Best Practices zur Containerisierung von Python-Backends + +Dieser Leitfaden zeigt einen professionellen Workflow zur Containerisierung einer Python-Backend-Anwendung mit Docker. Wir fokussieren uns auf die Integration in eine bereits bestehende Reverse-Proxy-Infrastruktur wie Traefik für die lokale Entwicklung. Wir verwenden eine minimale FastAPI-Anwendung als Beispiel. + +Die vorgestellten Methoden umfassen: +- Optimierte, sichere Docker-Images durch Multi-Stage-Builds. +- Einen einfachen Workflow durch die Verwendung eines `Makefile`. +- Trennung von Entwicklungs- und Produktionsumgebungen. +- Korrekte Verwaltung von Code und Images mit Git und einer Container Registry. +- Integration in eine bestehende Traefik-Infrastruktur für dynamisches Routing. + +## 1. Projektstruktur + +Wir haben die folgende Struktur erstellt: + +``` +/ +├── .dockerignore # Listet Dateien auf, die Docker ignorieren soll. +├── .gitignore # Listet Dateien auf, die Git ignorieren soll. +├── Dockerfile # Die Blaupause für unser produktives Docker-Image. +├── Makefile # Vereinfacht die Ausführung von Docker-Befehlen. +├── app/ +│ ├── __init__.py # Macht 'app' zu einem Python-Paket. +│ └── main.py # Unser FastAPI-Anwendungscode. +├── docker-compose.yml # Definiert die lokale Entwicklungsumgebung (integriert sich in externen Traefik). +├── guide.md # Dieser Leitfaden. +└── requirements.txt # Liste der Python-Abhängigkeiten. +``` + +## 2. Die Komponenten im Detail + +### `requirements.txt` +Hier definieren wir die Python-Bibliotheken, die unser Projekt benötigt. +``` +fastapi +uvicorn[standard] +gunicorn +``` +- `fastapi`: Das Web-Framework. +- `uvicorn`: Ein schneller ASGI-Server, ideal für die Entwicklung. +- `gunicorn`: Ein robuster WSGI-Produktionsserver, der Uvicorn-Worker zur Ausführung unserer ASGI-Anwendung nutzt. + +### `.gitignore` & `.dockerignore` +- **`.gitignore`**: Verhindert, dass sensible Daten (`.env`), temporäre Dateien (`__pycache__`) oder lokale Konfigurationen (`.vscode/`) in Git eingecheckt werden. +- **`.dockerignore`**: Verhindert, dass unnötige oder sensible Dateien in das Docker-Image kopiert werden. Dies hält das Image klein und sicher. Wir schließen z.B. das `.git`-Verzeichnis, das `Dockerfile` selbst und lokale Umgebungsdateien aus. + +### `app/main.py` +Eine minimale FastAPI-Anwendung mit zwei Endpunkten. +```python +from fastapi import FastAPI + +app = FastAPI() + +@app.get("/") +def read_root(): + return {"message": "Hello World"} + +@app.get("/health") +def health_check(): + return {"status": "ok"} +``` + +### `Dockerfile` (Multi-Stage-Build) +Dies ist das Herzstück unserer Containerisierung. Ein Multi-Stage-Build trennt die Build-Umgebung von der Laufzeitumgebung. + +**Stufe 1: `builder`** +```dockerfile +FROM python:3.11 as builder +WORKDIR /opt/venv +RUN python -m venv . +COPY requirements.txt . +RUN . /opt/venv/bin/activate && pip install --no-cache-dir -r requirements.txt +``` +- Wir starten mit einem vollständigen Python-Image. +- Erstellen ein virtuelles Environment (`venv`) und installieren die Abhängigkeiten hinein. +- Das Ergebnis ist ein Ordner `/opt/venv` mit einer sauberen Python-Umgebung. + +**Stufe 2: `runner` (Das finale Image)** +```dockerfile +FROM python:3.11-slim +RUN useradd --create-home --shell /bin/bash appuser +WORKDIR /home/appuser/app +COPY --from=builder /opt/venv /opt/venv +COPY app/ . +ENV PATH="/opt/venv/bin:$PATH" +EXPOSE 8000 +USER appuser +CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "-w", "2", "-b", "0.0.0.0:8000", "main:app"] +``` +- Wir starten mit einem minimalen `-slim`-Image. +- **Sicherheit**: Wir erstellen einen unprivilegierten Benutzer `appuser`. Die Anwendung wird als dieser Benutzer ausgeführt. +- Wir kopieren das `venv` aus der `builder`-Stufe und unseren App-Code. +- **Produktions-Server**: Wir starten die App mit `gunicorn` und 2 `uvicorn`-Workern. Dies ist ein stabiles Setup für die Produktion. + +### `docker-compose.yml` (Für die lokale Entwicklung, Integration in externen Traefik) +Diese Datei definiert unsere lokale Entwicklungsumgebung für die Anwendung und integriert sie in eine **bereits bestehende und extern verwaltete Traefik-Instanz**. + +**Konzept der Integration:** +Anstatt Traefik bei jedem Start der Anwendung neu zu deployen, gehen wir davon aus, dass Traefik bereits als zentraler Reverse Proxy auf dem Host-System oder in einem separaten `docker-compose`-Setup läuft. Unsere Anwendung verbindet sich einfach mit dem von Traefik bereitgestellten Docker-Netzwerk und Traefik erkennt die Anwendung über Labels. + +```yaml +version: '3.8' + +services: + app: + build: . + volumes: + - ./app:/home/appuser/app + command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] + labels: + - "traefik.enable=true" + # Definiert einen Router für den App-Dienst + - "traefik.http.routers.app-router.rule=Host(`localhost`)" + - "traefik.http.routers.app-router.entrypoints=web" + # Definiert den internen Port des Dienstes (im Container) + - "traefik.http.services.app-service.loadbalancer.server.port=8000" + networks: + - web + +networks: + web: + external: true # Dieses Netzwerk wird von einer externen Traefik-Instanz bereitgestellt +``` +- **`app` Service**: Konfiguriert unsere Anwendung. Die Labels sind entscheidend für die Erkennung durch Traefik: + - `traefik.enable=true`: Aktiviert die Erkennung durch Traefik. + - `traefik.http.routers.app-router.rule=Host('localhost')`: Sagt Traefik, dass Anfragen an den Host `localhost` an diesen Dienst geleitet werden sollen. Dies kann auf einen spezifischen Domainnamen angepasst werden, wenn Traefik entsprechend konfiguriert ist. + - `traefik.http.routers.app-router.entrypoints=web`: Verknüpft diesen Router mit dem `web`-Entrypoint von Traefik (üblicherweise Port 80). + - `traefik.http.services.app-service.loadbalancer.server.port=8000`: Informiert Traefik über den internen Port des FastAPI-Dienstes. +- **`networks: web`**: Deklariert ein **externes Netzwerk**. Das bedeutet, dass dieses Netzwerk nicht von diesem `docker-compose.yml` erstellt, sondern von einem anderen Dienst (in unserem Fall Traefik) bereitgestellt wird. Unsere Anwendung tritt diesem Netzwerk bei. + +--- + +**Beispiel für eine zentrale Traefik-Bereitstellung (separates `docker-compose.yml`)** + +Damit die obige Konfiguration funktioniert, muss eine Traefik-Instanz laufen, die das `web`-Netzwerk bereitstellt. Hier ist ein Beispiel, wie eine solche zentrale Traefik-Instanz bereitgestellt werden könnte (dies ist ein *separates* `docker-compose.yml`, das an anderer Stelle ausgeführt wird): + +```yaml +# docker-compose-traefik.yml (Beispiel für zentrale Traefik-Bereitstellung) +version: '3.8' + +services: + traefik: + image: "traefik:v2.10" + container_name: traefik + command: + - "--api.dashboard=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" # Nur Dienste mit traefik.enable=true exposen + - "--entrypoints.web.address=:80" + - "--entrypoints.websecure.address=:443" # Optional: Für HTTPS + ports: + - "80:80" # HTTP + - "443:443" # Optional: HTTPS + - "8080:8080" # Traefik Dashboard + volumes: + - "/var/run/docker.sock:/var/run/docker.sock:ro" # Traefik braucht Zugriff auf den Docker-Daemon + # - "./traefik-certs:/etc/traefik/certs" # Optional: Für HTTPS Zertifikate + networks: + - web + +networks: + web: + external: false # Dieses Netzwerk wird von dieser Traefik-Instanz erstellt und verwaltet +``` +Dieses `docker-compose-traefik.yml` würde in einem eigenen Verzeichnis gestartet (`docker-compose up -d`) und würde das `web`-Netzwerk erzeugen, in das sich dann unsere Anwendungs-`docker-compose.yml` einklinken kann. + +--- + +### `Makefile` (Der Kommando-Runner) +Das `Makefile` gibt uns einfache Befehle für komplexe Aktionen. Es wurde angepasst, um die Integration in einen externen Traefik widerzuspiegeln. Die spezifischen Traefik-Port-Checks wurden entfernt, da diese außerhalb der Verantwortung dieser Anwendung liegen. + +```makefile +# --- Configuration --- +# Use the current directory name as the default image name +IMAGE_NAME := $(shell basename $(CURDIR)) +# Default tag +TAG := latest +# Placeholder for a remote registry (e.g., your Docker Hub username or Gitea instance) +# For Gitea: REGISTRY ?= your-gitea-host:port/owner +# Example: REGISTRY ?= localhost:3000/myuser +REGISTRY ?= your-registry-username + +# Default internal application port (exposed to Traefik) +PORT := 8000 + +# Function to find next free host port for the app and ask user +# This function will echo the chosen port if successful, or exit with an error. +define check_app_port_free + @local start_port=$(PORT); \ + local found_port=$$start_port; \ + local is_free=false; \ + \ + if ! ss -tulnp | grep ":$$start_port " > /dev/null; then \ + echo "$$start_port"; \ + exit 0; \ + fi; \ + \ + echo "Port $$start_port is busy. Searching for a free port for the app..."; >&2; \ + while ! $$is_free; do \ + if ! ss -tulnp | grep ":$$found_port " > /dev/null; then \ + is_free=true; \ + else \ + ((found_port++)); \ + if [ "$$found_port" -gt 65535 ]; then \ + echo "ERROR: No free ports found up to 65535. Aborting." >&2; \ + exit 1; \ + fi; \ + fi; \ + done; \ + \ + echo "Port $$start_port is busy. I found port $$found_port to be free for the app." >&2; \ + read -p "Do you want to use port $$found_port for the app? (y/N): " choice; \ + case "$$choice" in \ + y|Y ) \ + echo "$$found_port"; \ + ;; \ + * ) \ + echo "Operation cancelled by user." >&2; \ + exit 1; \ + ;; \ + esac; +endef + +# --- Docker Commands --- + +.PHONY: build +build: + @echo "Building Docker image: $(IMAGE_NAME):$(TAG)" + docker build -t $(IMAGE_NAME):$(TAG) . + +.PHONY: run +run: + @export SELECTED_HOST_PORT=$$(bash -c 'func() { $(check_app_port_free) }; func') && \ + echo "Using host port $$SELECTED_HOST_PORT for single app container" && \ + docker run -d -p $$SELECTED_HOST_PORT:$(PORT) --name $(IMAGE_NAME) $(IMAGE_NAME):$(TAG) + +.PHONY: stop +stop: + @echo "Stopping Docker container: $(IMAGE_NAME)" + docker stop $(IMAGE_NAME) || true + docker rm $(IMAGE_NAME) || true + +.PHONY: logs +logs: + @echo "Showing logs for container: $(IMAGE_NAME)" + docker logs -f $(IMAGE_NAME) + +.PHONY: shell +shell: + @echo "Accessing shell in container: $(IMAGE_NAME)" + docker exec -it $(IMAGE_NAME) /bin/bash + +# --- Docker Compose Commands --- + +.PHONY: up +up: + @echo "Starting development environment with Docker Compose (integrating with external Traefik)..." + docker-compose up --build -d + +.PHONY: down +down: + @echo "Stopping development environment with Docker Compose..." + docker-compose down + +# --- Image Management --- + +.PHONY: tag +tag: + @echo "Tagging image $(IMAGE_NAME):$(TAG) as $(REGISTRY)/$(IMAGE_NAME):$(TAG)" + docker tag $(IMAGE_NAME):$(TAG) $(REGISTRY)/$(IMAGE_NAME):$(TAG) + +.PHONY: push +push: tag + @echo "Pushing image $(REGISTRY)/$(IMAGE_NAME):$(TAG) to registry..." + docker push $(REGISTRY)/$(IMAGE_NAME):$(TAG) + +# --- Cleanup --- + +.PHONY: clean +clean: + @echo "Cleaning up stopped containers and dangling images..." + docker container prune -f + docker image prune -f + +.PHONY: help +help: + @echo "Available commands:" + @echo " build - Build the Docker image" + @echo " run - Run the Docker container (single app, no Traefik)" + @echo " stop - Stop and remove the Docker container" + @echo " logs - Follow the logs of the container" + @echo " shell - Get a shell inside the running container" + @echo " up - Start the dev environment with docker-compose (integrates with external Traefik)" + @echo " down - Stop the dev environment with docker-compose" + @echo " tag - Tag the image for a registry" + @echo " push - Push the image to a registry (after tagging)" + @echo " clean - Clean up unused containers and images" + @echo " help - Show this help message" + +.DEFAULT_GOAL := help diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9a86ac1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +fastapi +uvicorn[standard] +gunicorn \ No newline at end of file