Files
audio-engine-hub/docs/guide.md
stephan 528a185d3b feat: Overhaul application and add DX improvements
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.
2025-12-04 17:33:44 +01:00

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 der README.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, das Dockerfile selbst 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/venv mit 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 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 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
  • app Service: Die direkte Port-Exponierung wurde entfernt. Stattdessen nutzt Traefik die labels, 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 Host localhost an diesen Dienst geleitet werden sollen.
    • traefik.http.routers.app-router.entrypoints=web: Verknüpft diesen Router mit dem web-Entrypoint von Traefik (Port 80).
    • traefik.http.services.app-service.loadbalancer.server.port=8000: Informiert Traefik über den internen Port des FastAPI-Dienstes.
  • traefik Service: 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.
  • networks: web: Erstellt ein gemeinsames Netzwerk, in dem Traefik und die app kommunizieren 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