diff --git a/.gitignore b/.gitignore index 8806507..3f2c607 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ .env.* !.env.example +# Entwicklungsmodus - gehoert nicht auf den Produktivserver +docker-compose.override.yml + # Docker-Secrets-Dateien secrets/ *.key @@ -40,4 +43,4 @@ dist/ .vscode/ *.swp .DS_Store -*.zip +*.zip \ No newline at end of file diff --git a/README.md b/README.md index 9bcfb0d..361d4c4 100644 --- a/README.md +++ b/README.md @@ -151,9 +151,28 @@ Dummy-Hash, damit die Antwortzeit konstant bleibt. hilft nicht gegen verteilte Angriffe auf ein einzelnes Konto; nur pro Konto zu begrenzen macht das Aussperren fremder Nutzer trivial. -**`restart: "no"` beim API-Container.** Während der Entwicklung soll ein -Startfehler zu einem stehenden Container führen, nicht zu einer Endlosschleife, -die das Log flutet. Für den Produktivbetrieb auf `unless-stopped` ändern. +**Produktivbetrieb ist die Voreinstellung.** Der `api`-Container läuft mit +`restart: unless-stopped`, ohne Bind-Mount und ohne `--reload` – es läuft der +Code aus dem gebauten Abbild. + +Zum Entwickeln: + +```bash +cp docker-compose.override.yml.example docker-compose.override.yml +docker compose up -d +``` + +Compose zieht eine vorhandene `docker-compose.override.yml` automatisch mit +heran. Sie bindet `./backend` ein, schaltet `--reload` an und setzt +`restart: "no"`, damit ein Startfehler zu einem stehenden Container führt statt +zu einer Endlosschleife im Log. Die Datei steht in `.gitignore` und gehört +nicht auf den Produktivserver. + +**Ohne Override wirken Änderungen am Quelltext erst nach einem Neubau:** + +```bash +docker compose up -d --build +``` **Selbstregistrierung: drei Zustände statt zwei.** `ALLOW_SELF_REGISTRATION` kennt `true`, `false` und `admin`. Bei `true`/`false` ist der Wert fest diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh index 2799423..768a469 100644 --- a/backend/entrypoint.sh +++ b/backend/entrypoint.sh @@ -7,7 +7,18 @@ python -m app.wait_for_db echo "[entrypoint] Migrationen einspielen ..." alembic upgrade head -echo "[entrypoint] Starte API." +# --reload nur beim Entwickeln. Im Dauerbetrieb bringt es einen +# zusaetzlichen Ueberwachungsprozess, dauerndes Absuchen des +# Dateisystems - und es setzt voraus, dass der Quelltext ueber ein +# Bind-Mount hereinkommt statt aus dem gebauten Abbild. +if [ "${DEV_RELOAD:-false}" = "true" ]; then + echo "[entrypoint] Starte API im Entwicklungsmodus (--reload)." + RELOAD_ARG="--reload" +else + echo "[entrypoint] Starte API." + RELOAD_ARG="" +fi + # --proxy-headers: X-Forwarded-For auswerten, damit das Rate Limiting # die echte Client-IP sieht und nicht die des nginx-Containers. # --forwarded-allow-ips: nur diesen Absendern glauben. Der Wert ist @@ -15,4 +26,4 @@ echo "[entrypoint] Starte API." exec uvicorn app.main:app \ --host 0.0.0.0 --port 8000 \ --proxy-headers --forwarded-allow-ips "${FORWARDED_ALLOW_IPS:-*}" \ - --reload + ${RELOAD_ARG} diff --git a/docker-compose.override.yml.example b/docker-compose.override.yml.example new file mode 100644 index 0000000..a7123e1 --- /dev/null +++ b/docker-compose.override.yml.example @@ -0,0 +1,34 @@ +# Entwicklungsmodus. +# +# cp docker-compose.override.yml.example docker-compose.override.yml +# docker compose up -d +# +# Docker Compose zieht eine vorhandene docker-compose.override.yml +# automatisch mit heran - ohne zusaetzliche Schalter. +# +# Diese Datei gehoert NICHT auf den Produktivserver: Mit dem Bind-Mount +# laeuft dort Code vom Host statt aus dem gebauten, geprueften Abbild. + +services: + api: + # Quelltext live einbinden und uvicorn bei Aenderungen neu starten + volumes: + - ./backend:/app + environment: + DEV_RELOAD: "true" + # Startfehler sollen zu einem stehenden Container fuehren, nicht zu + # einer Endlosschleife, die das Log flutet + restart: "no" + # Der Healthcheck stoert beim Entwickeln nur - waehrend eines + # Neustarts durch --reload waere der Container kurz "unhealthy" + healthcheck: + disable: true + + # API direkt erreichbar fuer curl und die interaktive Dokumentation. + # An 127.0.0.1 gebunden - sonst haengt eine ungeschuetzte API am + # oeffentlichen Interface. + # ports: + # - "127.0.0.1:8000:8000" + + web: + restart: "no" diff --git a/docker-compose.yml b/docker-compose.yml index f389c1d..cfbea2f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,9 +36,18 @@ services: depends_on: db: condition: service_healthy - volumes: - - ./backend:/app - restart: "no" + # Kein Bind-Mount und kein --reload: Es laeuft der Code aus dem + # gebauten Abbild. Zum Entwickeln docker-compose.override.yml + # anlegen (Vorlage: docker-compose.override.yml.example). + healthcheck: + # Kein curl im python:slim-Abbild - deshalb ueber Python. + test: ["CMD", "python3", "-c", + "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/healthz', timeout=3).status==200 else 1)"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 60s + restart: unless-stopped networks: [einkaufsapp] web: diff --git a/docs/betrieb.md b/docs/betrieb.md index 492ac52..55f680d 100644 --- a/docs/betrieb.md +++ b/docs/betrieb.md @@ -152,6 +152,64 @@ Clients die alte Fassung. geänderten Werte aus der `.env`. Das hat in diesem Projekt schon mehrfach für Verwirrung gesorgt. +## Wenn ein Container verschwindet + +```bash +docker compose ps -a +docker inspect einkaufsapp_api --format \ + 'Status={{.State.Status}} Exit={{.State.ExitCode}} OOM={{.State.OOMKilled}} Ende={{.State.FinishedAt}}' +docker compose logs --tail=50 api +``` + +Die letzten Zeilen im Log verraten den Grund: + +| Im Log | Bedeutung | +|---|---| +| `Shutting down` → `Application shutdown complete` → `Stopping reloader process` | Geordnetes Beenden. Jemand hat den Container gestoppt – SIGTERM an PID 1 | +| Nichts, `OOMKilled=true` | Vom Kernel wegen Speichermangels abgeschossen | +| Traceback, `Exit=1` | Startfehler, meist Konfiguration oder Migration | + +### Geordnetes Beenden aufspüren + +```bash +journalctl --since "yesterday 22:00" --until "today 07:00" | grep -iE "docker|compose|borg" +grep -rniE "docker (compose )?(stop|down)" /etc/cron* /root /opt 2>/dev/null +``` + +Häufigster Verursacher ist ein Sicherungsskript, das Container anhält. **Für +die Datenbanksicherung ist das nicht nötig:** `mariadb-dump +--single-transaction` liefert einen konsistenten Stand im laufenden Betrieb. + +### Was `restart: unless-stopped` leistet – und was nicht + +Der `api`-Container startet jetzt nach Abstürzen und nach einem Neustart des +Hosts von selbst wieder. + +**Nach einem ausdrücklichen `docker compose stop` aber nicht.** Das ist die +Bedeutung von „unless-stopped": Docker merkt sich, dass der Stopp gewollt war. +Wenn dein Sicherungsskript Container anhält, muss es sie danach selbst wieder +starten: + +```bash +docker compose stop +# ... sichern ... +docker compose start +``` + +Ein Skript, das nur stoppt und sich auf die Neustartregel verlässt, hinterlässt +eine tote Anwendung – genau das ist hier passiert. + +### Kehrseite der Neustartregel + +Bricht der Container beim Start ab, etwa wegen einer fehlgeschlagenen +Migration, versucht Docker es endlos erneut und das Log füllt sich mit +Wiederholungen. Dann hilft: + +```bash +docker compose stop api +docker compose logs --tail=60 api # in Ruhe lesen +``` + ## Speicherplatz im Blick behalten ```bash