Appearance
FrankenPHP + Caddy via Docker Compose (Standard)
Dies ist das produktive Setup für Staging und Production. Caddy übernimmt Reverse-Proxy + Auto-SSL für alle Domains; PHP-Workloads (Filament, Kirby, Livewire-B2B) laufen als FrankenPHP-Container im Worker-Mode. JS-Workloads (Nuxt, Vitepress, Storybook-Output) laufen in Bun- bzw. nginx-Containern. Verwaltet wird alles über docker-compose.yml-Dateien in home/bengel/stacks/, versioniert in Git. Portainer dient nur als Cockpit (Logs, Restart, Shell), nicht als Source-of-Truth.
Server-Layout
Es gibt zwei klar getrennte Hierarchien auf dem Host:
/home/bengel/stacks/ ← Compose + .env + persistente Daten
├── proxy/ (caddy + Caddyfile)
├── mgmt/ (portainer, matomo, matomo-db)
├── git/ (forgejo+db, woodpecker+agent)
└── apps/
├── docker-compose.yml ← referenziert /var/www/<service>/
├── penpot-compose.yml (Sub-Compose für Penpot)
├── .env (Passwörter, Keys — NICHT in Git)
├── apps_db_data/ ⚠ Volume — Backup!
├── apps_redis_data/
├── meili_data/ ⚠
├── opensearch_data/ ⚠
├── ollama_data/
├── penpot_postgres/ ⚠
└── penpot_assets/ ⚠
/var/www/ ← Source-Code pro Service (Webroot)
├── admin/ ← Filament-PIM (Laravel + Filament 5)
├── cms/ ← Kirby CMS + GraphQL
├── b2b/ ← Livewire-Händlerportal
├── staging/ ← Nuxt 4 Marketing-Frontend
├── docs/ ← VitePress
├── storybook/ ← statisches Storybook-Build
├── frontend/, portal/ ← Legacy, bleiben bis Cut-Over
├── shared/ ← Symlink-Targets / Schemas
├── design/, project/, tracking/ ← (Service-Volumes — nicht Code)
└── jeder Ordner: bengel:www-data 2775 (setgid, group-writable)Warum diese Trennung?
/var/www/<service>/ist die Konvention des Servers (bengel:www-data2775). Da liegen Code, Dockerfiles,.envder einzelnen Apps.home/bengel/stacks/<stack>/enthält nur die Orchestrierung —docker-compose.yml, gemeinsame Daten-Volumes (DB, Cache, Search-Indizes).- Build-Context im Compose zeigt auf
/var/www/<service>/→ das Image wird direkt aus dem Webroot gebaut, kein Doppel-Verzeichnis mit dem ganzen Source nötig.
Warum dieser Stack?
| Komponente | Vorteil |
|---|---|
| Caddy | Auto-SSL (Let's Encrypt + ZeroSSL fallback), HTTP/3 out-of-the-box, deklaratives Caddyfile statt Nginx-Configs |
| FrankenPHP + Octane | PHP bleibt im Speicher — kein Framework-Boot pro Request, ~2-5× schneller als php-fpm |
| Docker Compose pro Stack | klare Trennung (proxy / mgmt / git / apps), Stack einzeln neu starten/deployen/sichern |
| Git-based Compose | jede Änderung im Diff sichtbar, Rollback per git revert, DR = git clone + Volumes-Restore |
| Portainer (read/inspect) | Logs, Stats, Container-Shell ohne SSH; aber niemals Stack im Portainer-UI editieren — immer im Repo |
Externes Docker-Netz gateway | Caddy spricht jeden Container per Name an, ohne Port-Mapping |
1. Server-Vorbereitung (einmalig)
bash
# OS aktualisieren
apt update && apt upgrade -y
# Docker
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker
# Deploy-User (kein Passwort, nur SSH-Key)
adduser --disabled-password --gecos "" bengel
usermod -aG docker bengel
mkdir -p /home/bengel/.ssh
echo "ssh-ed25519 AAAA... dev-laptop" >> /home/bengel/.ssh/authorized_keys
chown -R bengel:bengel /home/bengel/.ssh
chmod 700 /home/bengel/.ssh && chmod 600 /home/bengel/.ssh/authorized_keys
# /var/www/ existiert ohnehin — sicherstellen, dass setgid + group write für www-data:
chown -R bengel:www-data /var/www
find /var/www -type d -exec chmod 2775 {} \;
find /var/www -type f -exec chmod 0664 {} \;
# Firewall: 80/443 öffentlich, 9443/9000 (Portainer) nur für Admin-IPs
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80,443/tcp
ufw allow 443/udp # HTTP/3
ufw allow from <admin-ip-1> to any port 9443 proto tcp
ufw allow from <admin-ip-1> to any port 9000 proto tcp
ufw enableWARNING
Portainer niemals offen ins Internet hängen. Entweder Firewall-Whitelist (oben), oder besser: per Tailscale/Wireguard-VPN, dann nur über tailscale0-Interface erreichbar.
2. Externes Docker-Netz + Stacks ziehen
bash
su - bengel
# Externes Netz, an dem alle Stacks hängen
docker network create gateway
# Stack-Configs holen (heute: aus dem Infrastructure-Repo, perspektivisch
# eigener `matev/server-stacks`-Repo)
mkdir -p ~/stacks
cd ~/stacks
# Variante A: per Submodule-Subdir aus dem Hauptrepo
rsync -av <dev-laptop>:/path/to/matev_2026/home/bengel/stacks/ ./
# Variante B: separates Repo, sobald Forgejo stabil
# git clone https://git.matev.eu/matev/server-stacks.git ..env-Dateien für jeden Stack lokal erzeugen — Werte aus dem Passwort-Manager, nicht in Git:
bash
cp ~/stacks/mgmt/.env.example ~/stacks/mgmt/.env && nano ~/stacks/mgmt/.env
cp ~/stacks/git/.env.example ~/stacks/git/.env && nano ~/stacks/git/.env
cp ~/stacks/apps/.env.example ~/stacks/apps/.env && nano ~/stacks/apps/.env3. Stack proxy/ — Caddy
Aktueller ~/stacks/proxy/Caddyfile (zentrale Routing-Tabelle):
caddyfile
{
email bengel@bengel.digital
auto_https off # auf "on" stellen sobald DNS produktiv zeigt
servers { trusted_proxies static private_ranges }
}
# apps/
http://admin.matev.eu { reverse_proxy admin:80 }
http://cms.matev.eu { reverse_proxy cms:80 }
http://b2b.matev.eu { reverse_proxy b2b:80 }
http://staging.matev.eu { reverse_proxy staging:3000 }
http://storybook.matev.eu { reverse_proxy storybook:80 }
http://docs.matev.eu { reverse_proxy docs:3000 }
http://design.matev.eu { reverse_proxy penpot-frontend:80 }
http://project.matev.eu { reverse_proxy taiga-gateway:80 }
# mgmt/
http://analytics.matev.eu { reverse_proxy matomo:80 }
# Portainer ist NICHT hier — direkt https://<server-ip>:9443
# git/
http://git.matev.eu { reverse_proxy forgejo:3000 }
http://ci.matev.eu { reverse_proxy woodpecker:8000 }Hochfahren:
bash
cd ~/stacks/proxy
docker compose up -d
docker compose exec caddy caddy reload --config /etc/caddy/CaddyfileINFO
Sobald DNS auf den Server zeigt, auto_https off entfernen — Caddy holt dann per HTTP-01-Challenge automatisch Let's-Encrypt-Zertifikate für jeden Hostname.
4. Stack mgmt/ — Portainer + Matomo
Portainer läuft off-domain — direkt über die Server-IP, kein Caddy-Eintrag. Vorteil: kein Caddy/Portainer-Port-Konflikt, kein DNS-Eintrag, Admin-Plane bleibt sauber separiert. Self-Signed-TLS auf 9443, plain HTTP auf 9000.
bash
cd ~/stacks/mgmt
docker compose up -d
# Portainer-Setup-Wizard: https://<server-ip>:9443 (Self-Signed-Warnung akzeptieren)
# Matomo-Setup-Wizard: https://analytics.matev.eu5. Stack git/ — Forgejo + Woodpecker
Bleibt strukturell wie heute (siehe home/bengel/stacks/git/docker-compose.yml). OAuth2-Client legt man in Forgejo unter Site Administration → Applications an, mit Redirect-URL https://ci.matev.eu/authorize.
bash
cd ~/stacks/git
docker compose up -d6. Stack apps/ — die Anwendungen
Die docker-compose.yml referenziert die Build-Contexts unter /var/www/<service>/. Die Dockerfiles liegen direkt im Webroot. Im home/bengel/stacks/apps/dockerfiles-reference/ liegen Vorlagen, die du nach /var/www/<service>/Dockerfile übertragen kannst.
Container-Convention
WORKDIR /var/www/html(Standard für FrankenPHP/Laravel/Kirby)- Container läuft als
www-datamit angeglichener UID 33 / GID 33 (Host-Standard) → Bind-Mounts in/var/www/<service>/storageetc. funktionieren ohne Permission-Drama - Code wird per
COPYins Image gebacken (deterministisch, schneller Start) - Bind-Mounts decken nur veränderliche Pfade ab:
storage/,content/,media/,.env
dockerfile
# Auszug aus jedem PHP-Dockerfile — UID-Angleichung
RUN deluser www-data \
&& addgroup -g 33 -S www-data \
&& adduser -u 33 -S -G www-data www-data
USER www-dataHochfahren (Demo-Reihenfolge)
bash
cd ~/stacks/apps
# 1) DB + Cache zuerst
docker compose up -d apps-db apps-redis
# 2) Filament-PIM
docker compose build admin
docker compose up -d admin
docker compose exec admin php artisan migrate --force
docker compose exec admin php artisan db:seed --force
docker compose exec admin php artisan filament:optimize
docker compose exec admin php artisan octane:reload
# 3) Kirby-CMS
docker compose build cms
docker compose up -d cms
# Erstes Setup im Panel: https://cms.matev.eu/panel
# 4) Storybook (statisch — Build muss in /var/www/storybook/ liegen)
docker compose up -d storybook
# 5) Penpot (Sub-Compose)
docker compose -f penpot-compose.yml up -d
# 6) später: b2b, staging, docs
docker compose build b2b staging docs
docker compose up -d b2b staging docs
# 7) Taiga (eigener Stack, separates Verzeichnis)
cd ~/stacks/taiga
docker compose up -dWas im admin-Container fertig ist
Der Filament-Container baut auf einer existierenden Code-Basis in /var/www/admin/ mit folgendem Funktionsumfang:
- Filament 5.6 + Laravel 13.6 + PHP 8.4
- Spatie: laravel-tags, laravel-settings, laravel-permission
- bezhansalleh/filament-shield (Permission-UI im Panel)
- laravel/sanctum (REST-API + Cross-Domain-Auth)
- 22 Pimcore-Importer (
php artisan import:pimcore --phase=1..5) - Search-Stack: laravel/scout + Meilisearch + OpenSearch + Ollama-Provider
- Console-Commands:
import:pimcore,blueprints:generate,opensearch:setup,ollama:setup,search:reindex,search:index-all - Spatie-Media-Library Pro-Vorbereitung (Migration + Trait)
Details in docs/LOCAL_DEV.md.
Smoke-Tests
bash
curl -I https://admin.matev.eu # → 302 zu /admin/login
curl -I https://cms.matev.eu # → 200
curl -I https://storybook.matev.eu # → 2007. Update-Workflow
bash
# 1) Stack-Definitionen aus Git ziehen
cd ~/stacks && git pull
# 2) Externe Images aktualisieren (matomo, mariadb, postgres, …)
cd ~/stacks/mgmt && docker compose pull && docker compose up -d
cd ~/stacks/git && docker compose pull && docker compose up -d
cd ~/stacks/apps && docker compose pull && docker compose up -d
# 3) Selbstgebaute Images neu bauen — wenn sich /var/www/<service>/-Code geändert hat
cd ~/stacks/apps && docker compose build admin && docker compose up -d admin
docker compose exec admin php artisan octane:reload # alter Worker-Code rauswerfenIn Portainer wird jeder Restart sichtbar; die UI nicht zum Editieren der Stacks benutzen — Source of Truth bleibt das Git-Repo.
Verwandte Themen
- Stacks-Referenz — pro Stack: Container, Volumes, Env-Vars, Backup-Kritikalität
- Backup-Strategie — Volumes + DB-Dumps + Restic
- Deployment — Woodpecker-Pipeline →
git pull+docker compose pull && up -d - Staging-Deploy — Demo-Pfad ohne Woodpecker
- Demo-Skript — Walkthrough für den Kunden