restart-condition in container-services

This commit is contained in:
2026-08-09 11:17:30 +02:00
parent 7b21e2d1b4
commit dd51532533
6 changed files with 143 additions and 9 deletions

5
.gitignore vendored
View File

@@ -8,6 +8,9 @@
.env.* .env.*
!.env.example !.env.example
# Entwicklungsmodus - gehoert nicht auf den Produktivserver
docker-compose.override.yml
# Docker-Secrets-Dateien # Docker-Secrets-Dateien
secrets/ secrets/
*.key *.key
@@ -40,4 +43,4 @@ dist/
.vscode/ .vscode/
*.swp *.swp
.DS_Store .DS_Store
*.zip *.zip

View File

@@ -151,9 +151,28 @@ Dummy-Hash, damit die Antwortzeit konstant bleibt.
hilft nicht gegen verteilte Angriffe auf ein einzelnes Konto; nur pro Konto zu hilft nicht gegen verteilte Angriffe auf ein einzelnes Konto; nur pro Konto zu
begrenzen macht das Aussperren fremder Nutzer trivial. begrenzen macht das Aussperren fremder Nutzer trivial.
**`restart: "no"` beim API-Container.** Während der Entwicklung soll ein **Produktivbetrieb ist die Voreinstellung.** Der `api`-Container läuft mit
Startfehler zu einem stehenden Container führen, nicht zu einer Endlosschleife, `restart: unless-stopped`, ohne Bind-Mount und ohne `--reload` es läuft der
die das Log flutet. Für den Produktivbetrieb auf `unless-stopped` ändern. 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` **Selbstregistrierung: drei Zustände statt zwei.** `ALLOW_SELF_REGISTRATION`
kennt `true`, `false` und `admin`. Bei `true`/`false` ist der Wert fest kennt `true`, `false` und `admin`. Bei `true`/`false` ist der Wert fest

View File

@@ -7,7 +7,18 @@ python -m app.wait_for_db
echo "[entrypoint] Migrationen einspielen ..." echo "[entrypoint] Migrationen einspielen ..."
alembic upgrade head 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 # --proxy-headers: X-Forwarded-For auswerten, damit das Rate Limiting
# die echte Client-IP sieht und nicht die des nginx-Containers. # die echte Client-IP sieht und nicht die des nginx-Containers.
# --forwarded-allow-ips: nur diesen Absendern glauben. Der Wert ist # --forwarded-allow-ips: nur diesen Absendern glauben. Der Wert ist
@@ -15,4 +26,4 @@ echo "[entrypoint] Starte API."
exec uvicorn app.main:app \ exec uvicorn app.main:app \
--host 0.0.0.0 --port 8000 \ --host 0.0.0.0 --port 8000 \
--proxy-headers --forwarded-allow-ips "${FORWARDED_ALLOW_IPS:-*}" \ --proxy-headers --forwarded-allow-ips "${FORWARDED_ALLOW_IPS:-*}" \
--reload ${RELOAD_ARG}

View File

@@ -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"

View File

@@ -36,9 +36,18 @@ services:
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
volumes: # Kein Bind-Mount und kein --reload: Es laeuft der Code aus dem
- ./backend:/app # gebauten Abbild. Zum Entwickeln docker-compose.override.yml
restart: "no" # 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] networks: [einkaufsapp]
web: web:

View File

@@ -152,6 +152,64 @@ Clients die alte Fassung.
geänderten Werte aus der `.env`. Das hat in diesem Projekt schon mehrfach für geänderten Werte aus der `.env`. Das hat in diesem Projekt schon mehrfach für
Verwirrung gesorgt. 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 ## Speicherplatz im Blick behalten
```bash ```bash