docs: update design doc + add infrastructure runbook
- Remove Ollama as AI backend (never used); Mistral is primary throughout - Add infrastructure.md: full Docker setup, queue architecture, prod deployment steps, env vars — prod has no staging ERP container Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
bf1af0a0bf
commit
371213dbbb
2 changed files with 560 additions and 24 deletions
|
|
@ -29,8 +29,8 @@ src/
|
||||||
# Orchestriert Domain über Interfaces (Ports)
|
# Orchestriert Domain über Interfaces (Ports)
|
||||||
Infrastructure/
|
Infrastructure/
|
||||||
Channel/ # EbayAdapter, FrappeErpAdapter, [FutureAdapter]
|
Channel/ # EbayAdapter, FrappeErpAdapter, [FutureAdapter]
|
||||||
AI/ # OllamaClient, MistralClient (beide hinter OllamaClientInterface)
|
AI/ # MistralClient hinter OllamaClientInterface (Interfacename historisch)
|
||||||
# OllamaVisionAgent, SpecsResearchAgent, JsonCodingAgent, EbayTextAgent
|
# VisionAgent, SpecsResearchAgent, JsonCodingAgent, EbayTextAgent
|
||||||
Persistence/ # Doctrine Repositories (PostgreSQL)
|
Persistence/ # Doctrine Repositories (PostgreSQL)
|
||||||
Logging/ # DatabaseLogHandler, ArchiveCommand
|
Logging/ # DatabaseLogHandler, ArchiveCommand
|
||||||
Http/ # Symfony Controller, Webhook-Listener, EasyAdmin
|
Http/ # Symfony Controller, Webhook-Listener, EasyAdmin
|
||||||
|
|
@ -38,7 +38,7 @@ src/
|
||||||
Storage/ # StorageManager
|
Storage/ # StorageManager
|
||||||
```
|
```
|
||||||
|
|
||||||
Jeder externe Dienst (eBay, Frappe ERP, Ollama, SMTP) implementiert ein Interface aus dem Application-Layer. Neue Plattform = neue Adapter-Klasse, Domain bleibt unberührt.
|
Jeder externe Dienst (eBay, Frappe ERP, Mistral, SMTP) implementiert ein Interface aus dem Application-Layer. Neue Plattform = neue Adapter-Klasse, Domain bleibt unberührt.
|
||||||
|
|
||||||
### 2.2 Tech-Stack
|
### 2.2 Tech-Stack
|
||||||
|
|
||||||
|
|
@ -51,7 +51,7 @@ Jeder externe Dienst (eBay, Frappe ERP, Ollama, SMTP) implementiert ein Interfac
|
||||||
| Tests | PHPUnit 11 + Pest |
|
| Tests | PHPUnit 11 + Pest |
|
||||||
| Datenbank | PostgreSQL 17 (eine Instanz, drei Schemas: `app`, `logs`, `logs_archive`) |
|
| Datenbank | PostgreSQL 17 (eine Instanz, drei Schemas: `app`, `logs`, `logs_archive`) |
|
||||||
| Cache / Queue | Redis 7 |
|
| Cache / Queue | Redis 7 |
|
||||||
| AI | **Ollama** (lokal, SSH-Tunnel + autossh) **oder Mistral Cloud API** — per Alias in `services.yaml` umschaltbar; Modelle via `AI_TEXT_MODEL` / `AI_VISION_MODEL` env vars konfigurierbar |
|
| AI | **Mistral Cloud API** — Vision: `pixtral-12b-2409`, Text: `mistral-large-latest`; Modelle via `AI_TEXT_MODEL` / `AI_VISION_MODEL` env vars konfigurierbar |
|
||||||
| Web-Suche | Tavily API (`TAVILY_API_KEY`) — liefert strukturierte Suchergebnisse für SpecsResearchAgent |
|
| Web-Suche | Tavily API (`TAVILY_API_KEY`) — liefert strukturierte Suchergebnisse für SpecsResearchAgent |
|
||||||
| Admin-Panel | EasyAdmin 5 (`#[AdminDashboard]` Attribut, `type: easyadmin.routes` Loader) |
|
| Admin-Panel | EasyAdmin 5 (`#[AdminDashboard]` Attribut, `type: easyadmin.routes` Loader) |
|
||||||
| Auth | Symfony Security + scheb/two-factor-bundle (TOTP) |
|
| Auth | Symfony Security + scheb/two-factor-bundle (TOTP) |
|
||||||
|
|
@ -71,7 +71,7 @@ redis://...?queue_name=orders # Order-Processing, CustomerResolver, Invo
|
||||||
redis://...?queue_name=channel_sync # Listing-Publish, Bestand-Sync, Deaktivierung, Tracking
|
redis://...?queue_name=channel_sync # Listing-Publish, Bestand-Sync, Deaktivierung, Tracking
|
||||||
```
|
```
|
||||||
|
|
||||||
**Ollama-Ausfall:** AI-Pipeline-Jobs verbleiben in der Queue. Symfony Messenger wiederholt mit Backoff, nach 3 Versuchen → Failed Transport (persistent, nicht verloren). Operativer Betrieb (Verkäufe, Rechnungen, Bestand) läuft völlig unabhängig durch. Replay via `messenger:failed:retry` wenn Ollama wieder verfügbar.
|
**AI-Backend-Ausfall:** AI-Pipeline-Jobs verbleiben in der Queue. Symfony Messenger wiederholt mit Backoff, nach 3 Versuchen → Failed Transport (persistent, nicht verloren). Operativer Betrieb (Verkäufe, Rechnungen, Bestand) läuft völlig unabhängig durch. Replay via `messenger:failed:retry` wenn das AI-Backend wieder erreichbar ist.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -216,14 +216,14 @@ ApiKey
|
||||||
|
|
||||||
```
|
```
|
||||||
1. Foto-Upload → PhotoUploadMessage in Queue (redis://ai_pipeline)
|
1. Foto-Upload → PhotoUploadMessage in Queue (redis://ai_pipeline)
|
||||||
2. OllamaVisionAgent — LLaVA liest Typenschild
|
2. VisionAgent — Mistral Pixtral liest Typenschild
|
||||||
Output: Hersteller, Modellname, Modellnummer, Seriennummer
|
Output: Hersteller, Modellname, Modellnummer, Seriennummer
|
||||||
3. Model-Cache-Check — findCompletedByModelNumber() in DB
|
3. Model-Cache-Check — findCompletedByModelNumber() in DB
|
||||||
Treffer → copy ebayTitle/ebayDescription/specsText/attributes → Schritt 6 (kein AI mehr)
|
Treffer → copy ebayTitle/ebayDescription/specsText/attributes → Schritt 6 (kein AI mehr)
|
||||||
Kein Treffer → weiter mit Schritt 4
|
Kein Treffer → weiter mit Schritt 4
|
||||||
4. SpecsResearchAgent — Tavily-Suche mit Modellbezeichnung → vollständige Specs (Freitext)
|
4. SpecsResearchAgent — Tavily-Suche mit Modellbezeichnung → vollständige Specs (Freitext)
|
||||||
Pflichtfeld-Liste kommt aus ArticleType.AttributeDefinitions ({{fields}}-Platzhalter im Prompt)
|
Pflichtfeld-Liste kommt aus ArticleType.AttributeDefinitions ({{fields}}-Platzhalter im Prompt)
|
||||||
5. JsonCodingAgent — strukturierter Ollama-Call: Specs-Text → JSON gegen ArticleType-Schema
|
5. JsonCodingAgent — strukturierter Mistral-Call: Specs-Text → JSON gegen ArticleType-Schema
|
||||||
6. ValidationGate — alle Pflichtfelder gesetzt? (Schema + eBay-Kategorie-Pflichtfelder)
|
6. ValidationGate — alle Pflichtfelder gesetzt? (Schema + eBay-Kategorie-Pflichtfelder)
|
||||||
✓ → Schritt 7
|
✓ → Schritt 7
|
||||||
✗ → Retry ab Schritt 5 mit missing_fields im Prompt (max. 3×)
|
✗ → Retry ab Schritt 5 mit missing_fields im Prompt (max. 3×)
|
||||||
|
|
@ -352,7 +352,7 @@ Schema: logs_archive.log_entry # identische Struktur
|
||||||
### Dienste
|
### Dienste
|
||||||
- **PostgreSQL:** App-User mit minimalen Rechten (kein Superuser)
|
- **PostgreSQL:** App-User mit minimalen Rechten (kein Superuser)
|
||||||
- **Redis:** `requirepass` gesetzt, nur intern erreichbar
|
- **Redis:** `requirepass` gesetzt, nur intern erreichbar
|
||||||
- **Ollama:** lokal, Zugriff nur via SSH-Tunnel (autossh für Persistenz + Auto-Reconnect)
|
- **Mistral API:** Key in `.env.local`, nie in Git; HTTPS-only
|
||||||
- **Gitea:** Registrierung deaktiviert, nur Admin-Anlage, SSH-Key-Auth
|
- **Gitea:** Registrierung deaktiviert, nur Admin-Anlage, SSH-Key-Auth
|
||||||
|
|
||||||
### Applikation
|
### Applikation
|
||||||
|
|
@ -440,31 +440,21 @@ docker compose exec app php bin/console app:api-keys:create --env=prod
|
||||||
Der Key wird als bcrypt-Hash gespeichert. Prefix (erste 8 Zeichen) dient als Lookup-Key.
|
Der Key wird als bcrypt-Hash gespeichert. Prefix (erste 8 Zeichen) dient als Lookup-Key.
|
||||||
Verwendung: `X-Api-Key: <rawKey>` HTTP-Header.
|
Verwendung: `X-Api-Key: <rawKey>` HTTP-Header.
|
||||||
|
|
||||||
### 11.3 AI-Backend wechseln
|
### 11.3 AI-Konfiguration
|
||||||
|
|
||||||
**Ollama (Standard, lokal):**
|
**Primär: Mistral Cloud API** (Standard, bereits so konfiguriert):
|
||||||
```yaml
|
```yaml
|
||||||
# config/services.yaml
|
# config/services.yaml
|
||||||
App\Infrastructure\AI\OllamaClientInterface:
|
|
||||||
alias: App\Infrastructure\AI\OllamaClient
|
|
||||||
```
|
|
||||||
```dotenv
|
|
||||||
# .env
|
|
||||||
AI_TEXT_MODEL=${OLLAMA_TEXT_MODEL}
|
|
||||||
AI_VISION_MODEL=${OLLAMA_VISION_MODEL}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Mistral Cloud API:**
|
|
||||||
```yaml
|
|
||||||
App\Infrastructure\AI\OllamaClientInterface:
|
App\Infrastructure\AI\OllamaClientInterface:
|
||||||
alias: App\Infrastructure\AI\MistralClient
|
alias: App\Infrastructure\AI\MistralClient
|
||||||
```
|
```
|
||||||
```dotenv
|
```dotenv
|
||||||
AI_TEXT_MODEL=${MISTRAL_TEXT_MODEL}
|
# .env.local
|
||||||
AI_VISION_MODEL=${MISTRAL_VISION_MODEL}
|
|
||||||
MISTRAL_API_KEY=sk-...
|
MISTRAL_API_KEY=sk-...
|
||||||
|
TAVILY_API_KEY=tvly-...
|
||||||
|
# Modelle in .env vorbelegt: mistral-large-latest / pixtral-12b-2409
|
||||||
```
|
```
|
||||||
Vision erfordert Pixtral (`pixtral-12b-2409`). Nach Änderung: `cache:clear` + FPM reload.
|
Nach Schlüsseländerung: `docker compose exec app php bin/console cache:clear`.
|
||||||
|
|
||||||
### 11.4 Pipeline starten
|
### 11.4 Pipeline starten
|
||||||
|
|
||||||
|
|
|
||||||
546
docs/superpowers/specs/2026-05-19-infrastructure.md
Normal file
546
docs/superpowers/specs/2026-05-19-infrastructure.md
Normal file
|
|
@ -0,0 +1,546 @@
|
||||||
|
# SuperSeller3000 — Infrastruktur & Deployment
|
||||||
|
|
||||||
|
**Zuletzt aktualisiert:** 2026-05-19
|
||||||
|
**Zielumgebung:** VPS (Ubuntu/Debian), Docker Compose
|
||||||
|
**Domain:** `ss3k.schaunwama.de`
|
||||||
|
|
||||||
|
> Dieses Dokument beschreibt den vollständigen Stand der Docker-Infrastruktur und alle Schritte, um das System auf einem neuen Server hochzuziehen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Übersicht der Services
|
||||||
|
|
||||||
|
| Service | Image / Build | Zweck |
|
||||||
|
|---|---|---|
|
||||||
|
| `app` | Build `docker/app/Dockerfile` | PHP 8.4-FPM — Symfony-Applikation |
|
||||||
|
| `caddy` | `caddy:2-alpine` | Reverse Proxy, Auto-HTTPS (Let's Encrypt) |
|
||||||
|
| `postgres` | `postgres:17-alpine` | Datenbank (Schemas: `app`, `logs`, `logs_archive`) |
|
||||||
|
| `redis` | `redis:7-alpine` | Queue-Backend + Session-Cache |
|
||||||
|
| `worker-ai` | Build `docker/app/Dockerfile` | Messenger Worker — Transport `ai_pipeline` |
|
||||||
|
| `worker-orders` | Build `docker/app/Dockerfile` | Messenger Worker — Transport `orders` |
|
||||||
|
| `worker-channel` | Build `docker/app/Dockerfile` | Messenger Worker — Transport `channel_sync` |
|
||||||
|
| `cron` | Build `docker/app/Dockerfile` | Log-Rotation täglich (`app:logs:rotate`) |
|
||||||
|
|
||||||
|
**Nicht im Prod-Setup:** kein Staging-ERP-Container. Frappe ERP läuft extern (eigene Instanz, `FRAPPE_ERP_BASE_URL` in `.env.local`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Queue-Architektur (Symfony Messenger + Redis Streams)
|
||||||
|
|
||||||
|
Drei isolierte Redis-Streams — ein ausgefallener Worker blockiert die anderen nie.
|
||||||
|
|
||||||
|
### Transports & Retry-Strategie
|
||||||
|
|
||||||
|
| Transport | Redis Stream | Worker | max_retries | Delay / Backoff |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `ai_pipeline` | `ai_pipeline` | `worker-ai` | 3 | 2 s · ×2 |
|
||||||
|
| `orders` | `orders` | `worker-orders` | 5 | 1 s · ×2 |
|
||||||
|
| `channel_sync` | `channel_sync` | `worker-channel` | 5 | 2 s · ×2 · max 60 s |
|
||||||
|
| `failed` | `failed` | — | — | persistent, nie verloren |
|
||||||
|
|
||||||
|
Nach Erschöpfung aller Retries landet die Message im `failed`-Transport. Replay: `messenger:failed:retry`.
|
||||||
|
|
||||||
|
### Messages pro Transport
|
||||||
|
|
||||||
|
#### `ai_pipeline` → `worker-ai`
|
||||||
|
|
||||||
|
Die Pipeline-Schritte werden sequenziell als separate Messages dispatcht — jeder Handler dispatcht den Nachfolger.
|
||||||
|
|
||||||
|
| Message | Handler-Logik |
|
||||||
|
|---|---|
|
||||||
|
| `PhotoUploadMessage` | Startet Pipeline A: `VisionAgent` (Mistral Pixtral) liest Typenschild → Hersteller, Modell, SN |
|
||||||
|
| `SpecsResearchMessage` | `SpecsResearchAgent`: Tavily-Suche mit Modellbezeichnung → Specs-Freitext; Pflichtfelder aus `ArticleType.AttributeDefinitions` (`{{fields}}`-Platzhalter im Prompt) |
|
||||||
|
| `JsonCodingMessage` | `JsonCodingAgent`: Specs-Text → strukturiertes JSON gegen ArticleType-Schema |
|
||||||
|
| `ValidationMessage` | Pflichtfelder vollständig? → weiter; sonst Retry ab `JsonCodingMessage` (max. 3×, `missing_fields` im Prompt) → `needs_review` |
|
||||||
|
| `DraftArticleMessage` | Article anlegen (status: `draft`), Inventurnummer vergeben, Attribute + Foto persistieren |
|
||||||
|
| `EbayTextMessage` | `EbayTextAgent`: eBay-Titel + Beschreibung aus Attributen generieren, an Article speichern |
|
||||||
|
| `PxeInventoryMessage` | Startet Pipeline B (PXE): PXE-Dump direkt an `JsonCodingAgent` — SpecsResearch entfällt |
|
||||||
|
|
||||||
|
**Model-Cache:** Nach `PhotoUploadMessage` prüft `findCompletedByModelNumber()` die DB. Treffer → alle Daten kopieren, `SpecsResearch`/`JsonCoding`/`EbayText` überspringen.
|
||||||
|
**Fehlerfall:** `PipelineJobFailureListener` fängt `WorkerMessageFailedEvent` ab und setzt `AIPipelineJob.status = failed` mit dem echten Fehlertext.
|
||||||
|
|
||||||
|
#### `orders` → `worker-orders`
|
||||||
|
|
||||||
|
| Message | Handler-Logik |
|
||||||
|
|---|---|
|
||||||
|
| `OrderReceivedMessage` | Vollständiger 13-Schritt-Order-Flow: Idempotenz-Check → atomarer Inventory-Lock (`stock - 1`) → `CustomerResolver` (Matching-Kaskade) → Order anlegen → Frappe Sales Invoice → PDF abrufen → `StorageManager` → Invoice-Record → PDF per SMTP an Lieferant → `UpdateStockOnChannelsMessage` oder `DeactivateListingMessage` dispatchen → Order: `completed` |
|
||||||
|
|
||||||
|
#### `channel_sync` → `worker-channel`
|
||||||
|
|
||||||
|
| Message | Handler-Logik |
|
||||||
|
|---|---|
|
||||||
|
| `PublishToChannelMessage` | `ChannelAdapterRegistry` → richtiger Adapter (z.B. `EbayAdapter`) → `publishListing()` → `ebay_listing_id` an Article zurückschreiben |
|
||||||
|
| `UpdateStockOnChannelsMessage` | `updateStock()` auf allen aktiven Plattformen des Artikels |
|
||||||
|
| `DeactivateListingMessage` | `deactivateListing()` — wird ausgelöst wenn `stock = 0` nach Verkauf |
|
||||||
|
| `TrackingPushMessage` | `pushTracking()` → Plattform als versandt markieren → `tracking_pushed_to_ebay_at` setzen |
|
||||||
|
|
||||||
|
### AI-Backend-Ausfall
|
||||||
|
|
||||||
|
AI-Worker-Messages verbleiben in der Queue. Messenger wiederholt mit Backoff, nach 3 Versuchen → `failed`-Transport. Operativer Betrieb (Verkäufe, Rechnungen, Bestand) läuft völlig unabhängig durch. Replay via `messenger:failed:retry` wenn das AI-Backend wieder erreichbar ist.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Netzwerk
|
||||||
|
|
||||||
|
Alle Services kommunizieren über ein internes Docker-Bridge-Network. Nur Caddy exponiert Ports nach außen.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 172.18.0.0/24
|
||||||
|
gateway: 172.18.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Das Gateway `172.18.0.1` ist die Host-Adresse innerhalb des Docker-Netzes (erreichbar von allen Containern).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. docker-compose.yml (Prod)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/app/Dockerfile
|
||||||
|
user: "1000:1000"
|
||||||
|
environment:
|
||||||
|
HOME: /tmp
|
||||||
|
volumes:
|
||||||
|
- .:/var/www
|
||||||
|
- ./docker/app/php.ini:/usr/local/etc/php/conf.d/app.ini:ro
|
||||||
|
- ./docker/app/zz-fpm-pool.conf:/usr/local/etc/php-fpm.d/zzz-pool.conf:ro
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
env_file:
|
||||||
|
- path: .env
|
||||||
|
required: true
|
||||||
|
- path: .env.local
|
||||||
|
required: false
|
||||||
|
|
||||||
|
caddy:
|
||||||
|
image: caddy:2-alpine
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./docker/caddy/Caddyfile:/etc/caddy/Caddyfile
|
||||||
|
- .:/var/www
|
||||||
|
- ./docker/app/php.ini:/usr/local/etc/php/conf.d/app.ini:ro
|
||||||
|
- caddy_data:/data
|
||||||
|
depends_on:
|
||||||
|
- app
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
command: redis-server --requirepass ${REDIS_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
worker-ai:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/app/Dockerfile
|
||||||
|
user: "1000:1000"
|
||||||
|
environment:
|
||||||
|
HOME: /tmp
|
||||||
|
command: php bin/console messenger:consume ai_pipeline --time-limit=3600 --memory-limit=256M
|
||||||
|
volumes:
|
||||||
|
- .:/var/www
|
||||||
|
- ./docker/app/php.ini:/usr/local/etc/php/conf.d/app.ini:ro
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
env_file:
|
||||||
|
- path: .env
|
||||||
|
required: true
|
||||||
|
- path: .env.local
|
||||||
|
required: false
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
worker-orders:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/app/Dockerfile
|
||||||
|
user: "1000:1000"
|
||||||
|
environment:
|
||||||
|
HOME: /tmp
|
||||||
|
command: php bin/console messenger:consume orders --time-limit=3600 --memory-limit=256M
|
||||||
|
volumes:
|
||||||
|
- .:/var/www
|
||||||
|
- ./docker/app/php.ini:/usr/local/etc/php/conf.d/app.ini:ro
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
env_file:
|
||||||
|
- path: .env
|
||||||
|
required: true
|
||||||
|
- path: .env.local
|
||||||
|
required: false
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
worker-channel:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/app/Dockerfile
|
||||||
|
user: "1000:1000"
|
||||||
|
environment:
|
||||||
|
HOME: /tmp
|
||||||
|
command: php bin/console messenger:consume channel_sync --time-limit=3600 --memory-limit=256M
|
||||||
|
volumes:
|
||||||
|
- .:/var/www
|
||||||
|
- ./docker/app/php.ini:/usr/local/etc/php/conf.d/app.ini:ro
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
env_file:
|
||||||
|
- path: .env
|
||||||
|
required: true
|
||||||
|
- path: .env.local
|
||||||
|
required: false
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
cron:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/app/Dockerfile
|
||||||
|
user: "1000:1000"
|
||||||
|
environment:
|
||||||
|
HOME: /tmp
|
||||||
|
command: >
|
||||||
|
sh -c "while true; do
|
||||||
|
php bin/console app:logs:rotate;
|
||||||
|
sleep 86400;
|
||||||
|
done"
|
||||||
|
volumes:
|
||||||
|
- .:/var/www
|
||||||
|
- ./docker/app/php.ini:/usr/local/etc/php/conf.d/app.ini:ro
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
env_file:
|
||||||
|
- path: .env
|
||||||
|
required: true
|
||||||
|
- path: .env.local
|
||||||
|
required: false
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
redis_data:
|
||||||
|
caddy_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 172.18.0.0/24
|
||||||
|
gateway: 172.18.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hinweis:** `docker-compose.override.yml` existiert nur für lokale Entwicklung (Port-Bindings für Postgres/Redis zum direkten Zugriff vom Host). Auf dem Prod-Server nicht verwenden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Dockerfile — `docker/app/Dockerfile`
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM php:8.4-fpm-alpine
|
||||||
|
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
postgresql-dev \
|
||||||
|
icu-dev \
|
||||||
|
libzip-dev \
|
||||||
|
unzip \
|
||||||
|
git \
|
||||||
|
$PHPIZE_DEPS \
|
||||||
|
&& docker-php-ext-install \
|
||||||
|
pdo_pgsql \
|
||||||
|
intl \
|
||||||
|
zip \
|
||||||
|
opcache \
|
||||||
|
&& pecl install redis \
|
||||||
|
&& docker-php-ext-enable redis \
|
||||||
|
&& apk del $PHPIZE_DEPS
|
||||||
|
|
||||||
|
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
|
WORKDIR /var/www
|
||||||
|
|
||||||
|
COPY docker/app/php.ini /usr/local/etc/php/conf.d/app.ini
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. PHP-Konfiguration
|
||||||
|
|
||||||
|
### `docker/app/php.ini`
|
||||||
|
|
||||||
|
```ini
|
||||||
|
opcache.enable=0
|
||||||
|
memory_limit=256M
|
||||||
|
upload_max_filesize=20M
|
||||||
|
post_max_size=20M
|
||||||
|
```
|
||||||
|
|
||||||
|
`opcache.enable=0` weil der Dev-Build keinen Preload hat — in Prod kann auf `1` + `opcache.preload` umgestellt werden, wenn gewünscht.
|
||||||
|
|
||||||
|
### `docker/app/zz-fpm-pool.conf`
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[www]
|
||||||
|
; SSE connections hold a worker for up to 90 s each — raise the pool ceiling
|
||||||
|
; so regular requests are not starved.
|
||||||
|
pm = dynamic
|
||||||
|
pm.max_children = 30
|
||||||
|
pm.start_servers = 4
|
||||||
|
pm.min_spare_servers = 2
|
||||||
|
pm.max_spare_servers = 8
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Caddyfile (Prod) — `docker/caddy/Caddyfile`
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
admin off
|
||||||
|
}
|
||||||
|
|
||||||
|
ss3k.schaunwama.de {
|
||||||
|
root * /var/www/public
|
||||||
|
php_fastcgi app:9000
|
||||||
|
file_server
|
||||||
|
encode gzip
|
||||||
|
|
||||||
|
header {
|
||||||
|
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
X-Frame-Options "DENY"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prod enthält keinen `erpstaging`-Block.** Frappe ERP läuft auf einer eigenen Instanz (extern, `FRAPPE_ERP_BASE_URL` in `.env.local`).
|
||||||
|
|
||||||
|
> Aktueller Dev-Stand hat noch den `erpstaging.schaunwama.de`-Block für die Staging-ERP-Weiterleitung — der muss vor dem ersten Prod-Deploy entfernt werden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Umgebungsvariablen
|
||||||
|
|
||||||
|
### `.env` (ohne Credentials, in Git)
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
APP_ENV=prod
|
||||||
|
APP_SECRET=change_me_in_env_local
|
||||||
|
POSTGRES_DB=superseller
|
||||||
|
POSTGRES_USER=superseller
|
||||||
|
POSTGRES_PASSWORD=change_me
|
||||||
|
DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?serverVersion=17&charset=utf8"
|
||||||
|
REDIS_PASSWORD=change_me
|
||||||
|
REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379
|
||||||
|
MESSENGER_TRANSPORT_DSN=redis://:${REDIS_PASSWORD}@redis:6379/messages
|
||||||
|
MAILER_DSN=smtp://localhost
|
||||||
|
TAVILY_API_KEY=
|
||||||
|
MISTRAL_BASE_URL=https://api.mistral.ai
|
||||||
|
MISTRAL_API_KEY=
|
||||||
|
MISTRAL_VISION_MODEL=pixtral-12b-2409
|
||||||
|
MISTRAL_TEXT_MODEL=mistral-large-latest
|
||||||
|
AI_TEXT_MODEL=${MISTRAL_TEXT_MODEL}
|
||||||
|
AI_VISION_MODEL=${MISTRAL_VISION_MODEL}
|
||||||
|
EBAY_CLIENT_ID=
|
||||||
|
EBAY_CLIENT_SECRET=
|
||||||
|
EBAY_MARKETPLACE_ID=EBAY_DE
|
||||||
|
EBAY_API_BASE_URL=https://api.ebay.com
|
||||||
|
EBAY_OAUTH_BASE_URL=https://api.ebay.com
|
||||||
|
EBAY_VERIFICATION_TOKEN=
|
||||||
|
EBAY_ENDPOINT_URL=https://ss3k.schaunwama.de/webhooks/ebay
|
||||||
|
FRAPPE_ERP_BASE_URL=https://erp.example.com
|
||||||
|
FRAPPE_ERP_API_KEY=changeme
|
||||||
|
FRAPPE_ERP_API_SECRET=changeme
|
||||||
|
FRAPPE_GENERIC_ITEM_CODE=REFURB-HW
|
||||||
|
SUPPLIER_EMAIL=lieferant@example.com
|
||||||
|
SENDER_EMAIL=noreply@superseller3000.de
|
||||||
|
```
|
||||||
|
|
||||||
|
### `.env.local` (nur auf Server, nie in Git)
|
||||||
|
|
||||||
|
Alle echten Credentials überschreiben hier die Platzhalter aus `.env`:
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
APP_SECRET=<random-32-char-hex>
|
||||||
|
POSTGRES_PASSWORD=<sicheres-passwort>
|
||||||
|
REDIS_PASSWORD=<sicheres-passwort>
|
||||||
|
MAILER_DSN=smtp://user:pass@mailserver:587
|
||||||
|
TAVILY_API_KEY=tvly-...
|
||||||
|
MISTRAL_API_KEY=sk-...
|
||||||
|
EBAY_CLIENT_ID=...
|
||||||
|
EBAY_CLIENT_SECRET=...
|
||||||
|
EBAY_VERIFICATION_TOKEN=... # aus eBay Developer Portal
|
||||||
|
FRAPPE_ERP_BASE_URL=https://erp.meinefirma.de
|
||||||
|
FRAPPE_ERP_API_KEY=...
|
||||||
|
FRAPPE_ERP_API_SECRET=...
|
||||||
|
SUPPLIER_EMAIL=lieferant@meinefirma.de
|
||||||
|
SENDER_EMAIL=noreply@meinefirma.de
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. VPS-Voraussetzungen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Docker + Compose Plugin
|
||||||
|
apt install docker.io docker-compose-plugin
|
||||||
|
|
||||||
|
# User 1000 muss existieren (PHP läuft als 1000:1000)
|
||||||
|
id -u superseller # sollte 1000 sein; ggf. anpassen
|
||||||
|
|
||||||
|
# UFW: nur Caddy-Ports + SSH
|
||||||
|
ufw allow 22/tcp
|
||||||
|
ufw allow 80/tcp
|
||||||
|
ufw allow 443/tcp
|
||||||
|
ufw enable
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Erste Inbetriebnahme (Schritt für Schritt)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Repo klonen
|
||||||
|
git clone <gitea-url>/superseller3000 /home/superseller/SuperSeller3000
|
||||||
|
cd /home/superseller/SuperSeller3000
|
||||||
|
|
||||||
|
# 2. .env.local mit echten Credentials anlegen (siehe Abschnitt 7)
|
||||||
|
nano .env.local
|
||||||
|
|
||||||
|
# 3. Caddyfile prüfen — kein erpstaging-Block!
|
||||||
|
# (docker/caddy/Caddyfile muss nur den ss3k.schaunwama.de-Block enthalten)
|
||||||
|
|
||||||
|
# 4. Images bauen + DB hochfahren
|
||||||
|
docker compose up -d postgres redis
|
||||||
|
# warten bis postgres healthy
|
||||||
|
|
||||||
|
# 5. Composer install
|
||||||
|
docker compose run --rm app composer install --no-dev --optimize-autoloader
|
||||||
|
|
||||||
|
# 6. Datenbankmigrationen ausführen
|
||||||
|
docker compose run --rm app php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
|
||||||
|
# 7. Cache aufwärmen
|
||||||
|
docker compose run --rm app php bin/console cache:warmup --env=prod
|
||||||
|
|
||||||
|
# 8. Ersten Admin-User anlegen
|
||||||
|
docker compose run --rm app php bin/console app:users:create --env=prod
|
||||||
|
|
||||||
|
# 9. Ersten API-Key anlegen (Klartext wird nur einmal angezeigt)
|
||||||
|
docker compose run --rm app php bin/console app:api-keys:create --env=prod
|
||||||
|
|
||||||
|
# 10. Alle Services starten
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# 11. eBay-Webhook registrieren (URL in .env.local: EBAY_ENDPOINT_URL)
|
||||||
|
# Im eBay Developer Portal: Notification URL = https://ss3k.schaunwama.de/webhooks/ebay
|
||||||
|
# Verification Token = EBAY_VERIFICATION_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Laufender Betrieb
|
||||||
|
|
||||||
|
### Worker-Status prüfen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose ps
|
||||||
|
docker compose logs -f worker-ai
|
||||||
|
docker compose logs -f worker-orders
|
||||||
|
docker compose logs -f worker-channel
|
||||||
|
```
|
||||||
|
|
||||||
|
### Failed Messages (nach Erschöpfung aller Retries)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Übersicht
|
||||||
|
docker compose exec app php bin/console messenger:failed:show
|
||||||
|
|
||||||
|
# Einzelne Message erneut versuchen
|
||||||
|
docker compose exec app php bin/console messenger:failed:retry
|
||||||
|
|
||||||
|
# Alle nochmal
|
||||||
|
docker compose exec app php bin/console messenger:failed:retry --all
|
||||||
|
```
|
||||||
|
|
||||||
|
### Updates deployen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull
|
||||||
|
docker compose build app worker-ai worker-orders worker-channel cron
|
||||||
|
docker compose run --rm app composer install --no-dev --optimize-autoloader
|
||||||
|
docker compose run --rm app php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
docker compose run --rm app php bin/console cache:warmup --env=prod
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logs (Admin-Panel)
|
||||||
|
|
||||||
|
`https://ss3k.schaunwama.de/admin` → **Log Entries** — Fulltext-Suche, Filter nach Level/Channel/Zeitraum.
|
||||||
|
Rotation läuft täglich automatisch (Cron-Service): Einträge > 90 Tage → `logs_archive`, danach gelöscht.
|
||||||
|
|
||||||
|
### Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# PostgreSQL-Dump
|
||||||
|
docker compose exec postgres pg_dump -U superseller superseller | gzip > backup_$(date +%Y%m%d).sql.gz
|
||||||
|
|
||||||
|
# Artikel-Fotos und Rechnungs-PDFs (Docker Volumes)
|
||||||
|
docker run --rm -v superseller3000_postgres_data:/data -v $(pwd):/backup \
|
||||||
|
alpine tar czf /backup/volume_postgres_$(date +%Y%m%d).tar.gz /data
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. AI-Konfiguration
|
||||||
|
|
||||||
|
### Primäres Backend: Mistral Cloud API
|
||||||
|
|
||||||
|
Standardkonfiguration — `config/services.yaml` bereits so ausgeliefert:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
App\Infrastructure\AI\OllamaClientInterface:
|
||||||
|
alias: App\Infrastructure\AI\MistralClient
|
||||||
|
```
|
||||||
|
|
||||||
|
`.env.local`:
|
||||||
|
```dotenv
|
||||||
|
MISTRAL_API_KEY=sk-...
|
||||||
|
# Modelle sind in .env vorbelegt:
|
||||||
|
# AI_TEXT_MODEL=${MISTRAL_TEXT_MODEL} → mistral-large-latest
|
||||||
|
# AI_VISION_MODEL=${MISTRAL_VISION_MODEL} → pixtral-12b-2409
|
||||||
|
```
|
||||||
|
|
||||||
|
Web-Suche (SpecsResearchAgent): **Tavily API** — `TAVILY_API_KEY=tvly-...` in `.env.local`.
|
||||||
|
|
||||||
|
Nach Schlüsseländerung: `docker compose exec app php bin/console cache:clear`.
|
||||||
Loading…
Reference in a new issue