This commit introduces a wide range of improvements to the application, focusing on stability, developer experience (DX), and documentation.
Key changes include:
- **Fix Application Startup:** Resolved a critical bug where the FastAPI application instance was not correctly exposed, preventing Uvicorn from starting ().
- **Simplify Docker Compose:** Removed the integrated Traefik setup from the default to support users with existing reverse proxies and simplify the local development environment.
- **Improve Makefile:**
- Implemented a robust, automatic port-finding mechanism for Starting development environment on port 8001...
#1 [internal] load local bake definitions
#1 reading from stdin 534B done
#1 DONE 0.0s
#2 [internal] load build definition from Dockerfile
#2 transferring dockerfile: 1.22kB done
#2 WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 2)
#2 DONE 0.0s
#3 [internal] load metadata for docker.io/library/python:3.11
#3 DONE 0.7s
#4 [internal] load metadata for docker.io/library/python:3.11-slim
#4 DONE 0.7s
#5 [internal] load .dockerignore
#5 transferring context: 385B done
#5 DONE 0.0s
#6 [builder 1/4] FROM docker.io/library/python:3.11@sha256:bf2d36b8fb1b4a0b590b36736cdd8a6b5175b411bf135c42694ecd68ab8fed02
#6 DONE 0.0s
#7 [stage-1 1/6] FROM docker.io/library/python:3.11-slim@sha256:193fdd0bbcb3d2ae612bd6cc3548d2f7c78d65b549fcaa8af75624c47474444d
#7 DONE 0.0s
#8 [internal] load build context
#8 transferring context: 4.90kB done
#8 DONE 0.0s
#9 [builder 2/4] WORKDIR /opt/venv
#9 CACHED
#10 [stage-1 4/6] WORKDIR /home/appuser
#10 CACHED
#11 [stage-1 3/6] RUN useradd --create-home --shell /bin/bash appuser
#11 CACHED
#12 [stage-1 2/6] RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
#12 CACHED
#13 [stage-1 5/6] COPY --from=builder /opt/venv /opt/venv
#13 CACHED
#14 [builder 4/4] RUN python -m venv . && . /opt/venv/bin/activate && pip install --no-cache-dir -r requirements.txt
#14 CACHED
#15 [builder 3/4] COPY requirements.txt .
#15 CACHED
#16 [stage-1 6/6] COPY app/ ./app
#16 CACHED
#17 exporting to image
#17 exporting layers done
#17 writing image sha256:6cac7caac7fda2808672ad2f3d117d46d38c1d93013867858543ec74917857b7 done
#17 naming to docker.io/library/audioenginehub-app done
#17 DONE 0.0s
#18 resolving provenance for metadata file
#18 DONE 0.0s and Using host port 8000 for single app container
8a1c69e868e7f13b4c8c9948e81921b48efd9536f326d200b9e912fb12ff66e3 to prevent port conflicts.
- Added a target (Running health check on running container...
App container is running on port 8001.
Waiting for app to initialize...
ERROR: Failed to decode JSON from health endpoint.) to run post-deployment sanity checks against the running container's endpoint.
- Recommended using Starting development environment on port 8002...
#1 [internal] load local bake definitions
#1 reading from stdin 534B done
#1 DONE 0.0s
#2 [internal] load build definition from Dockerfile
#2 transferring dockerfile: 1.22kB done
#2 WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 2)
#2 DONE 0.0s
#3 [internal] load metadata for docker.io/library/python:3.11-slim
#3 DONE 0.1s
#4 [internal] load metadata for docker.io/library/python:3.11
#4 DONE 0.2s
#5 [internal] load .dockerignore
#5 transferring context: 385B done
#5 DONE 0.0s
#6 [builder 1/4] FROM docker.io/library/python:3.11@sha256:bf2d36b8fb1b4a0b590b36736cdd8a6b5175b411bf135c42694ecd68ab8fed02
#6 DONE 0.0s
#7 [stage-1 1/6] FROM docker.io/library/python:3.11-slim@sha256:193fdd0bbcb3d2ae612bd6cc3548d2f7c78d65b549fcaa8af75624c47474444d
#7 DONE 0.0s
#8 [internal] load build context
#8 transferring context: 1.09GB 5.1s
#8 transferring context: 1.66GB 7.9s done
#8 DONE 8.0s
#9 [builder 3/4] COPY requirements.txt .
#9 CACHED
#10 [builder 4/4] RUN python -m venv . && . /opt/venv/bin/activate && pip install --no-cache-dir -r requirements.txt
#10 CACHED
#11 [stage-1 4/6] WORKDIR /home/appuser
#11 CACHED
#12 [stage-1 3/6] RUN useradd --create-home --shell /bin/bash appuser
#12 CACHED
#13 [builder 2/4] WORKDIR /opt/venv
#13 CACHED
#14 [stage-1 2/6] RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
#14 CACHED
#15 [stage-1 5/6] COPY --from=builder /opt/venv /opt/venv
#15 CACHED
#16 [stage-1 6/6] COPY app/ ./app
#16 CACHED
#17 exporting to image
#17 exporting layers done
#17 writing image sha256:6cac7caac7fda2808672ad2f3d117d46d38c1d93013867858543ec74917857b7 done
#17 naming to docker.io/library/audioenginehub-app done
#17 DONE 0.0s
#18 resolving provenance for metadata file
#18 DONE 0.0s for reliable port detection.
- **Update Documentation:**
- Replaced the outdated (which contained old source code) with a comprehensive guide covering setup, usage, and commands.
- Added a note to to clarify that it describes an older, more advanced setup, pointing readers to the new for the current recommended workflow.
These changes address the service startup failures and significantly improve the project's usability and maintainability.
12 KiB
Hinweis: Dieser Leitfaden beschreibt das ursprüngliche, erweiterte Setup dieses Projekts mit einer direkten Traefik-Integration in
docker-compose.yml. Für die lokale Entwicklung wurde der Standard-Workflow vereinfacht. Die aktuell empfohlene Methode zur Inbetriebnahme des Dienstes finden Sie in derREADME.md. Dieser Leitfaden dient weiterhin als Referenz für fortgeschrittene Konfigurationen, bei denen ein Reverse-Proxy wie Traefik manuell integriert werden soll.
Leitfaden: Best Practices zur Containerisierung von Python-Backends
Dieser Leitfaden zeigt einen professionellen Workflow zur Containerisierung einer Python-Backend-Anwendung mit Docker, inklusive Integration eines Reverse Proxys (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 von Traefik für dynamisches Routing in der lokalen Entwicklung.
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 mit 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, dasDockerfileselbst und lokale Umgebungsdateien aus.
app/main.py
Eine minimale FastAPI-Anwendung mit zwei Endpunkten.
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
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/venvmit einer sauberen Python-Umgebung.
Stufe 2: runner (Das finale Image)
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
venvaus derbuilder-Stufe und unseren App-Code. - Produktions-Server: Wir starten die App mit
gunicornund 2uvicorn-Workern. Dies ist ein stabiles Setup für die Produktion.
docker-compose.yml (Für die lokale Entwicklung mit Traefik)
Diese Datei definiert unsere lokale Entwicklungsumgebung, die nun auch Traefik als Reverse Proxy enthält.
Was ist Traefik? Traefik ist ein moderner Edge Router und Reverse Proxy, der dynamisch Dienste auf der Grundlage ihrer Konfiguration (z.B. Docker-Labels) entdeckt. Er leitet Anfragen an die richtigen Container weiter, ohne dass man manuelle Konfigurationen in einer separaten Datei vornehmen muss.
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
traefik:
image: "traefik:v2.10"
command:
- "--api.dashboard=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false" # Nur Dienste mit traefik.enable=true exposen
- "--entrypoints.web.address=:80"
ports:
- "80:80" # Der HTTP-Port, über den Traefik lauscht
- "8080:8080" # Das Web UI (Dashboard) von Traefik
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro" # Traefik braucht Zugriff auf den Docker-Daemon
networks:
- web
networks:
web:
external: false
appService: Die direkte Port-Exponierung wurde entfernt. Stattdessen nutzt Traefik dielabels, um das Routing zu konfigurieren.traefik.enable=true: Aktiviert die Erkennung durch Traefik.traefik.http.routers.app-router.rule=Host('localhost'): Sagt Traefik, dass Anfragen an die Hostlocalhostan diesen Dienst geleitet werden sollen.traefik.http.routers.app-router.entrypoints=web: Verknüpft diesen Router mit demweb-Entrypoint von Traefik (Port 80).traefik.http.services.app-service.loadbalancer.server.port=8000: Informiert Traefik über den internen Port des FastAPI-Dienstes.
traefikService: Dies ist der Traefik-Container selbst.- Er konfiguriert das Dashboard (
--api.dashboard=true) und aktiviert den Docker-Provider (--providers.docker=true), der Container anhand ihrer Labels entdeckt. - Er lauscht auf Port 80 (HTTP) und 8080 (Dashboard).
/var/run/docker.sock: Ermöglicht Traefik die Kommunikation mit dem Docker-Daemon, um Container und deren Labels zu erkennen.
- Er konfiguriert das Dashboard (
networks: web: Erstellt ein gemeinsames Netzwerk, in dem Traefik und dieappkommunizieren können.
Makefile (Der Kommando-Runner)
Das Makefile gibt uns einfache Befehle für komplexe Aktionen. Es wurde angepasst, um die Traefik-Ports zu berücksichtigen.
# --- Configuration ---
# Default internal application port (exposed to Traefik)
PORT := 8000
# Traefik's external entrypoints
TRAEFIK_WEB_PORT := 80
TRAEFIK_DASHBOARD_PORT := 8080
# --- Port Checking Functions ---
# Function to check if required Traefik ports are free
# Exits if any are busy, does not suggest alternatives.
define check_traefik_ports_free
@echo "Checking if Traefik ports ($(TRAEFIK_WEB_PORT}, $(TRAEFIK_DASHBOARD_PORT}) are free..." >&2
@local busy_ports=""; \
if ss -tulnp | grep ":$(TRAEFIK_WEB_PORT) " > /dev/null; then \
busy_ports="$$busy_ports $(TRAEFIK_WEB_PORT)"; \
fi; \
if ss -tulnp | grep ":$(TRAEFIK_DASHBOARD_PORT) " > /dev/null; then \
busy_ports="$$busy_ports $(TRAEFIK_DASHBOARD_PORT)"; \
fi; \
if [ -n "$$busy_ports" ]; then \
echo "ERROR: The following Traefik ports are already in use: $$busy_ports. Please free them or stop Traefik if already running." >&2; \
exit 1; \
fi; \
@echo "Traefik ports are free." >&2
endef
# 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:
$(call check_traefik_ports_free) # Check Traefik ports before starting
@echo "Starting development environment with Docker Compose (Traefik enabled)..."
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 (with 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