Appearance
Stacks-Referenz
Pro Stack: was läuft, welche Volumes, welche Env-Vars, was sichern. Alle Stacks hängen am externen Docker-Netz gateway.
home/bengel/stacks/ ← Compose + .env + persistente Volumes
├── proxy/ → caddy
├── mgmt/ → portainer (off-domain), matomo (+db)
├── git/ → forgejo (+db), woodpecker (+agent)
└── apps/ → admin, cms, b2b, staging, storybook, docs, search-stack,
+ Sub-Compose: penpot-compose.yml; eigener Stack taiga/
/var/www/ ← Code-Webroots pro Service (bengel:www-data 2775)
├── admin/ cms/ b2b/ staging/ docs/ storybook/
├── design/ project/ tracking/ (Volumes für Tools)
├── shared/ frontend/ portal/ (Schemas / Legacy)proxy/
Zweck: zentraler Reverse-Proxy mit Auto-SSL für alle *.matev.eu-Hosts.
| Container | Image | Funktion |
|---|---|---|
caddy | caddy:2.11.2 | Reverse-Proxy, Auto-SSL, HTTP/3 |
Caddyfile: zentrale Routing-Tabelle (home/bengel/stacks/proxy/Caddyfile) — eine Zeile pro Service. Nach jeder Änderung:
bash
docker compose -f ~/stacks/proxy/docker-compose.yml exec caddy \
caddy reload --config /etc/caddy/CaddyfileVolumes (zu sichern):
| Pfad | Inhalt |
|---|---|
caddy_data (Docker-Volume) | ausgestellte Let's-Encrypt-Zertifikate, OCSP-Stapling-Cache |
caddy_config (Docker-Volume) | Caddy-Runtime-Config |
TIP
caddy_data ist nicht streng kritisch — bei Verlust holt Caddy alle Zertifikate beim ersten Request neu. Aber Let's Encrypt hat Rate-Limits (50 Zertifikate / Domain / Woche), daher trotzdem sichern.
Ports (Host): 80, 443, 443/udp.
mgmt/
Zweck: Container-Cockpit + Web-Analytics.
| Container | Image | Funktion |
|---|---|---|
portainer | portainer/portainer-ce:latest | UI für alle Stacks (Logs, Restart, Shell) |
matomo | matomo:latest | Web-Analytics |
matomo-db | mariadb:10.11 | Matomo-Datenbank |
Volumes (zu sichern):
| Pfad | Inhalt | Restore-Kritisch |
|---|---|---|
./portainer_data/ | Portainer-DB (Users, Endpoints, Settings) | mittel — kann neu eingerichtet werden |
./matomo_data/ | Matomo-Webroot (Plugins, config/config.ini.php) | hoch — Lizenz, Plugins, Custom-Configs |
./matomo_db_data/ | Matomo-MariaDB-Datendateien | kritisch — alle Tracking-Daten |
Env-Vars (mgmt/.env):
ini
MATOMO_DB_PASSWORD=<sicheres-passwort>
MATOMO_DB_ROOT_PASSWORD=<sicheres-root-passwort>Ports (Host):
9000— Portainer HTTP (intern, nur via UFW-Whitelist oder VPN)9443— Portainer HTTPS (Self-Signed-TLS, nur via UFW-Whitelist oder VPN)- keine weiteren — Matomo läuft hinter Caddy
Routen: https://analytics.matev.eu → matomo:80. Portainer ist nicht in der Caddyfile — direkt https://<server-ip>:9443.
DB-Backup: siehe mgmt/db-backup.sh — dumpt Matomo-DB nach mgmt/dumps/, der Ordner wird vom Volume-Backup mitgenommen.
git/
Zweck: Git-Hosting + CI. Liegt unter ~/stacks/git (User bengel) und ist als Repo matev/infra versioniert (Secrets via .env, gitignored; .env.example als Vorlage).
| Container | Image | Funktion |
|---|---|---|
forgejo | codeberg.org/forgejo/forgejo:15-rootless | Git-Hosting (Web-UI + interner SSH-Server) |
forgejo-db | postgres:15-alpine | Forgejo-Datenbank |
woodpecker | woodpeckerci/woodpecker-server:v3 | CI-Server (Web-UI) |
woodpecker_agent | woodpeckerci/woodpecker-agent:v3 | CI-Worker |
Kein separater
forgejo-runner— CI läuft über Woodpecker (der Agent ist der Runner). Woodpecker-Images sind aufv3gepinnt: das:latest-Tag wurde von Woodpecker entfernt (Container beendet sich sonst mit einer Tag-Hinweis-Meldung → Crash-Loop).
Volumes (zu sichern):
| Pfad | Inhalt | Restore-Kritisch |
|---|---|---|
./postgres_forgejo/ | Forgejo-Postgres-Daten | kritisch — Repos-Metadaten, Issues, PRs, OAuth-Apps, User |
./forgejo_data/ | Repos, LFS, Avatars, Attachments | kritisch |
./forgejo_config/ | app.ini etc. | hoch |
./woodpecker_data/ | Woodpecker-SQLite + Build-Logs | mittel — muss UID 1000 gehören (sonst kann der v3-Container die SQLite-DB nicht anlegen) |
Env-Vars (git/.env, alle Secrets dort — nicht ins Git):
ini
FORGEJO_DB_PASSWORD=<sicher>
# Forgejo-Security (Pflicht! sonst hängt Forgejo im Installer-Modus / API 404):
FORGEJO_SECRET_KEY=<forgejo generate secret SECRET_KEY>
FORGEJO_INTERNAL_TOKEN=<forgejo generate secret INTERNAL_TOKEN>
# Mailer (Forgejo → bengel.digital):
FORGEJO_MAILER_PASSWD=<mailbox-pw>
# Woodpecker ↔ Forgejo OAuth2-App:
WOODPECKER_FORGEJO_CLIENT=<oauth2-client-id>
WOODPECKER_FORGEJO_SECRET=<oauth2-client-secret>
WOODPECKER_AGENT_SECRET=<random-32-byte>
WOODPECKER_ADMIN=forgejo-adminIm docker-compose.yml zusätzlich gesetzt: FORGEJO__security__INSTALL_LOCK=true (sonst zeigt Forgejo die Installer-Seite und die /api/v1-Routen sind nicht gemountet), FORGEJO__server__ROOT_URL=https://git.matev.eu/, sowie der Mailer (PROTOCOL=smtps, SMTP_ADDR=bengel.digital, SMTP_PORT=465, USER/FROM=matev-git@bengel.digital).
Routen: https://git.matev.eu → forgejo:3000, https://ci.matev.eu → woodpecker (Port 8000). Forgejo ist nur über HTTPS erreichbar; es ist kein Host-SSH-Port veröffentlicht (docker port forgejo ist leer) → Git push/pull läuft über HTTPS mit Token, nicht über SSH.
Login/Admin: Forgejo-Admin = forgejo-admin (E-Mail bengel@bengel.digital). Woodpecker hat keinen eigenen Login — nur Forgejo-OAuth.
Woodpecker-Agent braucht Docker-Socket (/var/run/docker.sock) — kann damit beliebige Container starten und den Host kontrollieren. Niemals untrusted Repos Zugriff geben.
apps/
Zweck: alle Anwendungen + Search-Stack.
| Container | Image / Build-Context | Funktion | Domain |
|---|---|---|---|
admin | build: /var/www/admin (FrankenPHP) | Filament-PIM | admin.matev.eu |
cms | build: /var/www/cms (FrankenPHP) | Kirby CMS + GraphQL | cms.matev.eu |
b2b | build: /var/www/b2b (FrankenPHP) | Livewire-Händlerportal | b2b.matev.eu |
staging | build: /var/www/staging (Bun) | Nuxt 4 Marketing-Frontend | staging.matev.eu |
docs | build: /var/www/docs (Bun) | Vitepress-Doku | docs.matev.eu |
storybook | build: /var/www/storybook (Bun → nginx) | statisches Storybook-Build | storybook.matev.eu |
apps-db | mariadb:11 | DB für admin + b2b | (intern) |
apps-redis | redis:7-alpine | Cache, Queue, Session | (intern) |
meilisearch | getmeili/meilisearch:v1.16 | Schicht-B-Suche (Frontend) | (intern) |
opensearch | opensearchproject/opensearch:2.18.0 | Schicht-C (Geo, RAG, Konfigurator, Embeddings) | (intern) |
ollama | ollama/ollama:latest | LLM-Provider (lokal) | (intern) |
penpot-* | (Sub-Compose penpot-compose.yml) | Design-Tool | design.matev.eu |
taiga-* | (eigener Stack home/bengel/stacks/taiga/) | Projektmanagement | project.matev.eu |
Wichtige Konvention
- Source-Code liegt in
/var/www/<service>/— Eigentumbengel:www-data 2775 - Stack-Definition (Compose, .env, Daten) liegt in
home/bengel/stacks/apps/ - Build-Context im
docker-compose.ymlzeigt auf/var/www/<service>/ - Bind-Mounts decken nur veränderliche Pfade (
storage/,content/,media/,.env) ab - Container-User auf Host-GID 33 angepasst (siehe
dockerfiles-reference/admin.Dockerfile)
Volumes (zu sichern):
| Pfad | Inhalt | Restore-Kritisch |
|---|---|---|
~/stacks/apps/apps_db_data/ | MariaDB-Datendateien (matev_admin + matev_b2b) | kritisch |
~/stacks/apps/meili_data/ | Meilisearch-Indizes (Frontend-Suche) | hoch — sonst lange Reindex-Dauer |
~/stacks/apps/opensearch_data/ | OpenSearch-Indizes (Geo, RAG, Embeddings) | hoch |
~/stacks/apps/ollama_data/ | Ollama-Modelle (mehrere GB) | mittel — lassen sich neu pullen |
~/stacks/apps/penpot_postgres/ | Penpot-Daten | hoch |
~/stacks/apps/penpot_assets/ | Penpot-Assets | hoch |
/var/www/admin/storage/ | Filament-Uploads (storage/app/public) | kritisch |
/var/www/cms/content/ | Kirby-Content (Markdown + YAML) | kritisch — der CMS-Inhalt |
/var/www/cms/media/ | Kirby-Media | kritisch |
/var/www/cms/site/accounts/ | Kirby-User | hoch |
/var/www/b2b/storage/ | Livewire-Uploads | kritisch |
~/stacks/apps/apps_redis_data/ | Cache, Queue | niedrig — verzichtbar |
Env-Vars (apps/.env):
ini
APPS_DB_ROOT_PASSWORD=<sicher>
APPS_DB_PASSWORD=<sicher>
ADMIN_APP_KEY=base64:<via php artisan key:generate --show>
B2B_APP_KEY=base64:<via php artisan key:generate --show>
KIRBY_LICENSE=<lizenz-key oder leer>
MEILISEARCH_MASTER_KEY=<sicher>
OPENSEARCH_ADMIN_PASSWORD=<sicher, mind. 8 chars + sonderzeichen>
PENPOT_DB_PASSWORD=<sicher>Filament-Stack-Inhalt (admin)
Was im Image enthalten ist (Stand des /var/www/admin/-Codes):
- Filament 5.6 + Laravel 13.6 + PHP 8.4 (FrankenPHP/Octane)
- Spatie: laravel-tags, laravel-settings, laravel-permission
- bezhansalleh/filament-shield (Permission-UI im Panel)
- laravel/sanctum (REST-API + Cross-Domain-Auth gegen
staging.matev.euundb2b.matev.eu) - 22 Pimcore-Importer (
php artisan import:pimcore --phase=1..5) - Search-Stack-Anbindung: laravel/scout (→ meilisearch), OpenSearch-Service, Ollama-Provider
- Console-Commands:
import:pimcore,blueprints:generate,opensearch:setup,ollama:setup,search:reindex,search:index-all - Spatie-Media-Library Pro-Vorbereitung (Migration + Trait, Lizenz nachträglich)
Kirby + GraphQL
GraphQL kommt über das Plugin getkirby/kql, Endpoint dann unter https://cms.matev.eu/api/query. Nuxt-Staging holt sich die Daten via useFetch('/api/query', { method: 'POST', body: { query } }) — definiert in staging/composables/useCms.ts.
Taiga + Penpot
Penpot fährt als Sub-Compose im apps/-Stack, Taiga steht in einem eigenen Stack-Verzeichnis (zu viele Container für ein Sub-Compose; sauberere Pfad-Trennung der Volumes):
bash
# Penpot — Sub-Compose im apps/-Stack
cd ~/stacks/apps
docker compose -f penpot-compose.yml up -d
# Taiga — eigener Stack (taigaio/taiga-docker)
# https://github.com/taigaio/taiga-docker
# taiga-gateway (nginx) hängt zusätzlich am externen `gateway`-Netz.
cd ~/stacks/taiga
cp .env.example .env # Secrets setzen
docker compose up -d
# Erstes-Mal: Superuser anlegen
docker compose exec taiga-back python manage.py createsuperuserStack-Operations-Cheatsheet
bash
# Status aller Stacks
for s in proxy mgmt git apps; do
echo "=== $s ==="
docker compose -f ~/stacks/$s/docker-compose.yml ps
done
# Einzelnen Container neu bauen + starten (Code in /var/www/admin/ geändert)
cd ~/stacks/apps && docker compose build admin && docker compose up -d admin
docker compose exec admin php artisan octane:reload
# Logs streamen
docker compose -f ~/stacks/apps/docker-compose.yml logs -f admin
# Shell im Container
docker compose -f ~/stacks/apps/docker-compose.yml exec admin sh
# Caddy-Reload (kein Container-Restart nötig)
docker compose -f ~/stacks/proxy/docker-compose.yml exec caddy \
caddy reload --config /etc/caddy/Caddyfile
# Komplett-Rebuild eines Stacks (Volumes bleiben erhalten, sofern benannt)
cd ~/stacks/apps && docker compose down && docker compose up -d --buildUpdate-Workflow
bash
# 1) Stack-Definitionen aus Git ziehen
cd ~/stacks && git pull
# 2) Source-Code pro Service aktualisieren
cd /var/www/admin && git pull # bzw. rsync vom Dev-Rechner
cd /var/www/cms && git pull
# ...
# 3) 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
# 4) Selbstgebaute Images neu bauen
cd ~/stacks/apps && docker compose build admin cms b2b staging docs
docker compose up -d
docker compose exec admin php artisan octane:reload
docker compose exec b2b php artisan octane:reloadIn Portainer wird der Restart sichtbar; die UI nicht zum Editieren der Stacks benutzen — Source of Truth bleibt das Git-Repo.