restart-condition in container-services
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
25
README.md
25
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
|
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
|
||||||
|
|||||||
@@ -7,7 +7,18 @@ python -m app.wait_for_db
|
|||||||
echo "[entrypoint] Migrationen einspielen ..."
|
echo "[entrypoint] Migrationen einspielen ..."
|
||||||
alembic upgrade head
|
alembic upgrade head
|
||||||
|
|
||||||
|
# --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."
|
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}
|
||||||
|
|||||||
34
docker-compose.override.yml.example
Normal file
34
docker-compose.override.yml.example
Normal 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"
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user