SuperSeller3000/docs/docker.md
Simon Kuehn 36e7d02539
Some checks are pending
CI / test (push) Waiting to run
docs: add Docker layout and service interaction documentation
Covers all 8 services, image/Dockerfile, PHP config, FPM pool tuning,
volume layout, network topology, startup order, local dev override,
operational commands, and Gitea Actions CI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 11:22:53 +00:00

264 lines
9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Docker Layout & Service-Zusammenspiel
---
## Services im Überblick
```
┌─────────────────────────────────────────────────────────────────┐
│ Internet │
└────────────────┬────────────────────────────────────────────────┘
│ :80 / :443
┌──────▼──────┐
│ caddy │ Reverse Proxy, Auto-HTTPS, gzip, HSTS
└──────┬──────┘
│ php_fastcgi app:9000
┌──────▼──────┐ ┌────────────────┐
│ app │◄────────►│ postgres :5432│
│ PHP-FPM │ └────────────────┘
└──────┬──────┘ ┌────────────────┐
│ │ redis :6379 │
┌──────▼──────────┐ └────────────────┘
│ Redis Streams │◄─────────────────────┘
│ (3 transports) │
└──┬───────┬──────┘
┌────────▼──┐ ┌──▼──────────┐ ┌──────────────┐ ┌──────┐
│ worker-ai │ │worker-orders│ │worker-channel│ │ cron │
└───────────┘ └─────────────┘ └──────────────┘ └──────┘
```
---
## Service-Beschreibungen
### `app` — PHP-FPM (Web)
- Image: eigenes Build aus `docker/app/Dockerfile` (PHP 8.4-FPM-Alpine)
- Empfängt FastCGI-Requests von Caddy auf Port 9000
- Schreibt in `var/` (Cache, Logs, Uploads), mounted als Bind-Mount
- User `1000:1000`, `HOME=/tmp` — kein Root, keine root-owned Dateien auf dem Host
**Abhängigkeiten beim Start:**
```
app → postgres (healthcheck: pg_isready)
app → redis (healthcheck: redis-cli ping)
```
### `caddy` — Reverse Proxy
- Image: `caddy:2-alpine`
- Terminiert TLS (Let's Encrypt, automatisch erneuert) auf Port 80/443
- Leitet alle PHP-Requests per `php_fastcgi app:9000` weiter
- Serviert statische Dateien direkt aus `/var/www/public` (`file_server`)
- Mounted das Projekt-Verzeichnis read-only für statische Assets
- Security-Header: `Strict-Transport-Security`, `X-Content-Type-Options`, `X-Frame-Options`
- Caddyfile: `docker/caddy/Caddyfile`
**Domain-Konfiguration in `Caddyfile`:**
```
ss3k.schaunwama.de → PHP-FPM app:9000 (SuperSeller3000)
erpstaging.* → reverse_proxy 172.18.0.1:8080 (externes Frappe — nur lokal)
```
> Hinweis: `erpstaging.*`-Block ist lokal/staging — in Prod läuft Frappe extern, diesen Block dann entfernen oder auskommentieren.
### `postgres` — Datenbank
- Image: `postgres:17-alpine`
- Volume: `postgres_data` (persistent, überlebt `docker compose down`)
- Healthcheck verhindert, dass `app` startet bevor Postgres bereit ist
- Kein Port nach außen in Prod (nur in `docker-compose.override.yml` für lokales Psql)
### `redis` — Queue + Cache
- Image: `redis:7-alpine`
- Passwort via `REDIS_PASSWORD` env var (aus `.env`)
- Volume: `redis_data` (persistent — Messenger-Queues überleben Neustart)
- Healthcheck: `redis-cli -a $REDIS_PASSWORD ping`
- Kein Port nach außen in Prod
### `worker-ai` — AI-Pipeline-Worker
```bash
php bin/console messenger:consume ai_pipeline --time-limit=3600 --memory-limit=256M
```
Verarbeitet: `PhotoUpload → SpecsResearch → JsonCoding → Validation → DraftArticle → EbayText`, `PxeInventory`
Retries: max. 3, 2 s × 2 Backoff. Nach 3 Fehlern → `failed`-Transport.
### `worker-orders` — Bestellungs-Worker
```bash
php bin/console messenger:consume orders --time-limit=3600 --memory-limit=256M
```
Verarbeitet: `OrderReceived` (idempotency → stock lock → customer → Frappe invoice → PDF → SMTP → channel sync)
Retries: max. 5, 1 s × 2 Backoff.
### `worker-channel` — Channel-Sync-Worker
```bash
php bin/console messenger:consume channel_sync --time-limit=3600 --memory-limit=256M
```
Verarbeitet: `PublishToChannel`, `UpdateStockOnChannels`, `DeactivateListing`, `TrackingPush`
Retries: max. 5, 2 s × 2 Backoff, max. 60 s. Kritisch: Listing-Deaktivierung nach Verkauf.
### `cron` — Tägliche Jobs
```bash
sh -c "while true; do php bin/console app:logs:rotate; sleep 86400; done"
```
Rotiert Logs täglich: Einträge > 90 Tage mit Level > DEBUG → `logs_archive`, dann löschen.
Kein echter Cron-Daemon im Container — simpler Shell-Loop reicht für tägliche Aufgaben.
---
## Image / Dockerfile
`docker/app/Dockerfile` — gleiches Image für `app`, alle `worker-*` und `cron`:
```
php:8.4-fpm-alpine
+ postgresql-dev, icu-dev, libzip-dev, git, unzip
+ Extensions: pdo_pgsql, intl, zip, opcache
+ PECL: redis
+ Composer 2
WORKDIR /var/www
```
Alle Services mounten das Projekt-Verzeichnis als Bind-Mount (`.:/var/www`), kein `COPY .` im Dockerfile. Das bedeutet: Code-Änderungen ohne Rebuild wirksam — nur Cache leeren (`cache:clear`).
---
## PHP-Konfiguration
`docker/app/php.ini`:
```ini
opcache.enable=0 ; dev/CI: kein OPcache (sofort aktuelle Dateien)
memory_limit=256M
upload_max_filesize=20M
post_max_size=20M
```
> In Prod OPcache aktivieren (`opcache.enable=1`, `opcache.validate_timestamps=0`) für bessere Performance.
`docker/app/zz-fpm-pool.conf`:
```ini
pm = dynamic
pm.max_children = 30 ; SSE-Verbindungen (Pipeline-Stream) halten Worker bis 90 s —
pm.start_servers = 4 ; Pool groß genug damit normale Requests nicht verhungern
pm.min_spare_servers = 2
pm.max_spare_servers = 8
```
---
## Volumes
| Volume | Inhalt | Gelöscht bei |
|---|---|---|
| `postgres_data` | DB-Daten | `docker compose down -v` |
| `redis_data` | Queue-Streams, Symfony-Cache | `docker compose down -v` |
| `caddy_data` | TLS-Zertifikate (Let's Encrypt) | `docker compose down -v` |
| `.:/var/www` (Bind) | Code + `var/` (Cache, Logs, Uploads) | nie automatisch |
**Wichtig:** `var/uploads/` enthält Artikel-Fotos und Rechnungs-PDFs. Separat sichern — liegt nicht in einem named Volume, sondern im Bind-Mount.
---
## Netzwerk
Alle Services im selben Bridge-Network `172.18.0.0/24` (Gateway `172.18.0.1`).
- `172.18.0.1` = Host-Maschine (für externe Dienste wie Frappe ERP über `erpstaging.*`)
- Services kommunizieren über Hostname (z. B. `app:9000`, `postgres:5432`, `redis:6379`)
- Nach außen geöffnet: nur Port 80/443 via Caddy
---
## Lokale Entwicklung (`docker-compose.override.yml`)
Im Override werden PostgreSQL und Redis zusätzlich nach außen exponiert:
```yaml
postgres:
ports:
- "5432:5432"
redis:
ports:
- "6379:6379"
```
Damit sind direkte Verbindungen von der IDE (TablePlus, redis-cli) möglich. **Nicht in Prod** — auf dem Server existiert nur `docker-compose.yml`.
---
## Startup-Reihenfolge
```
redis (healthcheck) ──┐
├── app (FPM)
postgres (healthcheck)┘ │
caddy
worker-ai
worker-orders
worker-channel
cron
```
Worker und Cron haben kein Healthcheck auf `app` (sie brauchen nur DB + Redis direkt).
---
## Wichtige Betriebsbefehle
```bash
# Status aller Services
docker compose ps
# Logs eines Workers in Echtzeit
docker compose logs -f worker-ai
# Alle Logs (alle Services)
docker compose logs -f
# Fehlgeschlagene Messenger-Jobs anzeigen
docker compose exec app php bin/console messenger:failed:show
# Fehlgeschlagene Jobs wiederholen
docker compose exec app php bin/console messenger:failed:retry
# Migrations ausführen
docker compose exec app php bin/console doctrine:migrations:migrate --no-interaction
# Symfony-Cache leeren (nach Code-Änderungen)
docker compose exec app php bin/console cache:clear
# Container neu starten (z. B. nach config/services.yaml-Änderungen)
docker compose restart app worker-ai worker-orders worker-channel
# Kompletter Neustart (behält Volumes)
docker compose down && docker compose up -d
```
---
## Gitea Actions CI
`.gitea/workflows/ci.yml` — läuft auf jedem Push:
1. PHP 8.4-Alpine Container + PostgreSQL 17 + Redis 7 als Service-Container
2. `composer install`
3. PHP CS Fixer (dry-run) — schlägt fehl bei Stil-Verletzungen
4. PHPStan level 9 — schlägt fehl bei Typ-Fehlern
5. Doctrine Migrations (gegen Test-DB)
6. PHPUnit/Pest Unit + Integration Tests
CI läuft ohne eBay/Mistral/Frappe-Credentials — alle Integrationstests, die externe APIs benötigen, überspringen sich selbst via `markTestSkipped`.