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