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

9 KiB
Raw Blame 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

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

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

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

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:

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:

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:

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

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