SuperSeller3000/docs/docker.md

265 lines
9 KiB
Markdown
Raw Normal View History

# 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`.