From 9b2db18fa3d957575d2f278826eba40eb5e5628f Mon Sep 17 00:00:00 2001 From: stephan Date: Thu, 4 Dec 2025 11:54:41 +0100 Subject: [PATCH] first commit --- .dockerignore | 14 ++ .gitignore | 112 +++++++++++++++ DesktopPopOS | 7 + DesktopPopOS.pub | 1 + Dockerfile | 41 ++++++ Makefile | 131 ++++++++++++++++++ README.md | 61 ++++++++ app/__init__.py | 0 app/main.py | 11 ++ docker-compose.yml | 21 +++ gitea-registry-guide.md | 166 ++++++++++++++++++++++ guide.md | 300 ++++++++++++++++++++++++++++++++++++++++ requirements.txt | 3 + 13 files changed, 868 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 DesktopPopOS create mode 100644 DesktopPopOS.pub create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 app/__init__.py create mode 100644 app/main.py create mode 100644 docker-compose.yml create mode 100644 gitea-registry-guide.md create mode 100644 guide.md create mode 100644 requirements.txt 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