# Deployment — SlimCore Marketing Site > **Stand:** 2026-05-04 · **Zielgruppe:** Pascal Oelmann · **Adressat:** dieselbe Marketing-Site (`slimcore.io`) plus die anderen Marken-Sites (`digiformer.eu`, `slimsafe.io`, `fonboard.io`) als Folge-Schritte > **Pfad:** Forgejo-First. GitHub wird übersprungen — kein Übergangs-Setup, das später migriert werden muss. Dieses Dokument beschreibt wie die Marketing-Site live geht. Es ist Teil eines familienweiten Standards für **Tier 2 — Static Marketing-Sites** (siehe `docs/handoff.md` für die Tier-Definition). --- ## 0. Voraussetzungen — ein Mal pro Familie Diese Phasen sind nur beim ersten Mal nötig. Spätere Marken-Sites laufen direkt zu Phase B. ### Phase A — Forgejo-Server + Runner (~1 h) #### A.1 VPS bestellen Hetzner Cloud → neuer Server in **Falkenstein**: | Feld | Wert | |---|---| | Typ | CX22 (4 GB RAM / 2 vCPU / 40 GB SSD) | | Image | Ubuntu 24.04 LTS | | SSH-Key | dein Schlüssel + ed25519 (kein RSA) | | Cloud-Init | optional — das Setup unten ist manuell | | Backups | aktivieren (~€0,80/mo extra) | | Standort | Falkenstein (FSN1) | | Hostname | `forge.digiformer.eu` | | Private Network | `slimcore-infra` (10.0.0.0/24) — falls später shared mit SlimCore-Cluster | Kosten: ~€4/mo + €0,80 Backups. #### A.2 Server härten (~10 min) ```bash # SSH als root ssh root@ # System aktualisieren apt update && apt upgrade -y && apt autoremove -y # Deploy-User adduser --disabled-password --gecos "" deploy usermod -aG sudo,docker deploy mkdir -p /home/deploy/.ssh cp ~/.ssh/authorized_keys /home/deploy/.ssh/ chown -R deploy:deploy /home/deploy/.ssh chmod 600 /home/deploy/.ssh/authorized_keys # SSH härten — root-Login deaktivieren, nur Key-Auth sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config systemctl reload ssh # Firewall apt install -y ufw fail2ban ufw default deny incoming ufw default allow outgoing ufw allow OpenSSH ufw allow http ufw allow https ufw --force enable systemctl enable --now fail2ban # Docker installieren curl -fsSL https://get.docker.com | sh usermod -aG docker deploy ``` #### A.3 Forgejo + Caddy + Runner via Docker Compose `/home/deploy/forgejo/docker-compose.yml`: ```yaml services: forgejo: image: codeberg.org/forgejo/forgejo:9 container_name: forgejo restart: unless-stopped environment: USER_UID: '1000' USER_GID: '1000' FORGEJO__database__DB_TYPE: postgres FORGEJO__database__HOST: db:5432 FORGEJO__database__NAME: forgejo FORGEJO__database__USER: forgejo FORGEJO__database__PASSWD_FILE: /run/secrets/db_password FORGEJO__server__DOMAIN: forge.digiformer.eu FORGEJO__server__ROOT_URL: https://forge.digiformer.eu/ FORGEJO__server__SSH_DOMAIN: forge.digiformer.eu FORGEJO__service__DISABLE_REGISTRATION: 'true' FORGEJO__webhook__ALLOWED_HOST_LIST: 'private,*.slimcore.io,*.digiformer.eu' secrets: - db_password volumes: - ./data:/var/lib/gitea - ./config:/etc/gitea ports: - '127.0.0.1:3000:3000' - '0.0.0.0:2222:22' depends_on: - db db: image: postgres:16-alpine container_name: forgejo-db restart: unless-stopped environment: POSTGRES_DB: forgejo POSTGRES_USER: forgejo POSTGRES_PASSWORD_FILE: /run/secrets/db_password secrets: - db_password volumes: - ./pg-data:/var/lib/postgresql/data runner: image: code.forgejo.org/forgejo/runner:6 container_name: forgejo-runner restart: unless-stopped user: '1000:1000' volumes: - ./runner-data:/data - /var/run/docker.sock:/var/run/docker.sock command: /bin/sh -c 'sleep 5; forgejo-runner daemon' depends_on: - forgejo secrets: db_password: file: ./db_password.txt ``` ```bash # Auf dem Server sudo -u deploy bash cd /home/deploy/forgejo mkdir -p data config pg-data runner-data openssl rand -hex 24 > db_password.txt chmod 600 db_password.txt docker compose up -d # Erste Schritte docker compose logs -f forgejo # Browser: http://:3000 (über SSH-Tunnel oder kurz UFW öffnen) # Setup-Wizard durchklicken: Admin-User anlegen, kein Email für jetzt ``` #### A.4 Caddy als TLS-Proxy Auf demselben Server, separat: `/home/deploy/caddy/docker-compose.yml`: ```yaml services: caddy: image: caddy:2-alpine container_name: caddy restart: unless-stopped ports: - '80:80' - '443:443' volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy-data:/data - caddy-config:/config network_mode: host # damit Caddy localhost:3000 erreicht volumes: caddy-data: caddy-config: ``` `/home/deploy/caddy/Caddyfile`: ```caddy forge.digiformer.eu { reverse_proxy 127.0.0.1:3000 encode zstd gzip header Strict-Transport-Security "max-age=31536000" } ``` ```bash docker compose up -d # DNS A-Record forge.digiformer.eu → # Caddy holt automatisch Let's-Encrypt-Cert, ~30 s ``` #### A.5 Runner registrieren In der Forgejo-UI (`https://forge.digiformer.eu`): - **Site Administration → Actions → Runners → Create new Runner** - Token kopieren ```bash docker compose -f /home/deploy/forgejo/docker-compose.yml exec runner forgejo-runner register \ --no-interactive \ --instance https://forge.digiformer.eu \ --token \ --name forge-runner-1 \ --labels docker:docker://node:22-bookworm,ubuntu-latest:docker://node:22-bookworm ``` Verifikation: in Forgejo-UI sollte der Runner als **online** erscheinen. Damit ist die Forgejo-Plattform fertig. #### A.6 Backup einrichten Cron auf Forgejo-Server (User `deploy`): ```bash # /home/deploy/backup-forgejo.sh #!/bin/bash set -euo pipefail DATE=$(date +%Y-%m-%d-%H%M) DEST="/home/deploy/backups/forgejo-$DATE.tar.zst" docker exec forgejo gitea dump --type tar.zst --file - > "$DEST" # Push to Storage Box (siehe Storage-Box-Setup unten) rsync -e "ssh -p 23 -i /home/deploy/.ssh/storagebox" "$DEST" \ uXXXXXX@uXXXXXX.your-storagebox.de:/home/forgejo-backups/ # Lokale Retention: 7 Tage find /home/deploy/backups -name "forgejo-*.tar.zst" -mtime +7 -delete ``` ```bash chmod +x /home/deploy/backup-forgejo.sh crontab -e # 0 3 * * * /home/deploy/backup-forgejo.sh >> /var/log/forgejo-backup.log 2>&1 ``` --- ## Phase B — Marketing-VPS einrichten (~30 min) ### B.1 VPS bestellen Wieder Hetzner Cloud, gleiche Wahl wie A.1, aber Hostname `marketing.digiformer.eu`. Standort Falkenstein. ### B.2 Server härten Identisch zu A.2 — Deploy-User, Firewall, Docker. ### B.3 Caddy installieren Marketing-VPS hostet alle statischen Marken-Sites über einen einzigen Caddy. `/home/deploy/marketing/docker-compose.yml`: ```yaml services: caddy: image: caddy:2-alpine container_name: marketing-caddy restart: unless-stopped ports: - '80:80' - '443:443' volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - ./sites:/var/www:ro - caddy-data:/data - caddy-config:/config volumes: caddy-data: caddy-config: ``` `/home/deploy/marketing/Caddyfile`: ```caddy # — slimcore.io — slimcore.io, www.slimcore.io { root * /var/www/slimcore.io try_files {path} {path}/ /index.html file_server encode zstd gzip header { Strict-Transport-Security "max-age=31536000; includeSubDomains" X-Content-Type-Options nosniff Referrer-Policy strict-origin-when-cross-origin -Server } # Astro generiert /index.html, /en/index.html, etc. # Sitemap + robots gehen direkt handle /sitemap-*.xml { file_server } handle /robots.txt { file_server } # SPA-Fallback nicht nötig — alle Routen haben echte HTML-Files } # — digiformer.eu — (analog, sobald migriert) digiformer.eu, www.digiformer.eu { root * /var/www/digiformer.eu file_server encode zstd gzip header Strict-Transport-Security "max-age=31536000" } # — slimsafe.io — slimsafe.io, www.slimsafe.io { root * /var/www/slimsafe.io file_server encode zstd gzip header Strict-Transport-Security "max-age=31536000" } # — fonboard.io — fonboard.io, www.fonboard.io { root * /var/www/fonboard.io file_server encode zstd gzip header Strict-Transport-Security "max-age=31536000" } # Catch-all 404 für unbekannte Hostnames :80 { respond 404 } ``` ```bash sudo -u deploy bash cd /home/deploy/marketing mkdir -p sites/slimcore.io sites/digiformer.eu sites/slimsafe.io sites/fonboard.io docker compose up -d ``` ### B.4 SSH-Key für Forgejo-Runner → Marketing-VPS Forgejo's Self-hosted Runner muss per SSH auf den Marketing-VPS rsynce können. ```bash # Auf Forgejo-Server (deploy-user) ssh-keygen -t ed25519 -f ~/.ssh/marketing_deploy_key -N "" cat ~/.ssh/marketing_deploy_key.pub ``` ```bash # Auf Marketing-VPS (deploy-user) adduser --disabled-password --gecos "" rsync-deploy mkdir -p /home/rsync-deploy/.ssh echo "" >> /home/rsync-deploy/.ssh/authorized_keys # Restriktion: nur rsync, kein Shell-Login echo 'command="rrsync -wo /var/www" ' > /home/rsync-deploy/.ssh/authorized_keys chmod 700 /home/rsync-deploy/.ssh chmod 600 /home/rsync-deploy/.ssh/authorized_keys chown -R rsync-deploy:rsync-deploy /home/rsync-deploy/.ssh # rsync-Schreibrechte auf /var/www mkdir -p /var/www chown -R rsync-deploy:rsync-deploy /var/www ``` In Forgejo-UI für das `slimcore-website`-Repo: - **Settings → Secrets and Variables → Actions → New Secret**: - `MARKETING_SSH_KEY`: privater Schlüsselinhalt von `~/.ssh/marketing_deploy_key` auf Forgejo-Server - `MARKETING_HOST`: IP oder DNS des Marketing-VPS - `MARKETING_USER`: `rsync-deploy` --- ## Phase C — Slimcore-Website Repo deployen ### C.1 Repo nach Forgejo migrieren ```bash # Lokal, im slimcore-website-Folder git remote add forgejo git@forge.digiformer.eu:digiformer/slimcore-website.git git push --mirror forgejo # Damit ist alles drüben — Branches, Tags, History ``` In Forgejo-UI: - Repo `digiformer/slimcore-website` ist nun gespiegelt - **Repository → Settings → Mirror Settings**: aus, weil unsere Quelle jetzt Forgejo ist ### C.2 Workflow-Datei In diesem Repo bereits vorhanden: `.forgejo/workflows/deploy.yml` (siehe diese Datei für Details). Der Workflow wird automatisch erkannt, sobald das Repo in Forgejo liegt. Trigger: - `push` auf `main` → Test, Build, Deploy auf Marketing-VPS unter `slimcore.io` ### C.3 DNS-Records setzen Beim Domain-Registrar (idealerweise INWX, Brand-System §7.3 Greylist): | Record | Typ | Ziel | |---|---|---| | `slimcore.io` | A | `` | | `www.slimcore.io` | A | `` | | `digiformer.eu` (später) | A | `` | | `forge.digiformer.eu` | A | `` | | `marketing.digiformer.eu` | A | `` (für SSH-Zugriff) | Caddy holt sich beim ersten Request das Let's-Encrypt-Cert automatisch. ~30 s nach DNS-Auflösung. ### C.4 Erstes Deployment auslösen ```bash git checkout main git push forgejo main ``` In Forgejo-UI: - **Actions** Tab im Repo → laufender Workflow - Bei Erfolg ist `https://slimcore.io/` live ### C.5 Verifikation Auf einem beliebigen Rechner: ```bash curl -I https://slimcore.io/ # → 200 OK, HSTS-Header, server: Caddy curl https://slimcore.io/sitemap-index.xml | head # → Sitemap-XML curl https://slimcore.io/robots.txt # → robots.txt curl -I https://slimcore.io/en/ # → 200 OK ``` Plus: lokale Playwright-Tests gegen Production einmal laufen lassen: ```bash PLAYWRIGHT_BASE_URL=https://slimcore.io pnpm exec playwright test --grep "renders" ``` Falls alle 16 Render-Tests grün → Site ist produktionsreif. --- ## Phase D — Spätere Marken-Sites Sobald Phase A + B abgeschlossen, ist jede weitere Marketing-Site: 1. Repo in Forgejo anlegen / migrieren 2. `.forgejo/workflows/deploy.yml` aus diesem Repo kopieren, `slimcore.io` durch passende Domain ersetzen 3. DNS A-Record auf Marketing-VPS-IP zeigen 4. Caddyfile-Block auf Marketing-VPS ergänzen + `caddy reload` Das ist ~15 min pro Marke. Kein neues VPS, kein neues Forgejo-Setup. --- ## Phase E — SlimCore-App-Repo (cockpit) später migrieren Cockpit folgt demselben Prinzip aber komplexer (3 Production-VPS, Branch-Protection, secrets-Repo, Patroni-Migrations). Eigener Migrations-Plan in `cockpit/docs/infrastructure/forgejo-migration.md` — wird erst angegangen, wenn Marketing-Site stabil läuft. Forgejo + Runner sind dann schon da; nur noch Workflow-Portierung + Secrets-Re-Upload + Branch-Protection. --- ## Backup-Strategie pro Komponente | Was | Wie | Wohin | Frequenz | |---|---|---|---| | Forgejo-Daten (Repos + DB + Config) | `gitea dump` → tar.zst | Hetzner Storage Box Nürnberg | Täglich 03:00 | | Marketing-Site `/var/www/*` | Inhaltlich = Build-Output, immer aus Git wieder herstellbar | — | nicht nötig | | Caddy-Daten (Let's-Encrypt-Certs) | Volume-Snapshot | Storage Box | Wöchentlich | | Marketing-VPS-Config (`docker-compose.yml`, `Caddyfile`) | Im Forgejo-Repo `infra/marketing-vps/` | — | bei jedem Commit | **Storage Box:** Hetzner Storage Box BX21 (1 TB, Nürnberg, ~€10/mo). Ein gemeinsamer Account für alle Marken. SFTP/rsync-Zugriff. Sub-Pfade pro System: `/forgejo-backups/`, `/slimcore-app-backups/`, etc. --- ## Monitoring (vorerst minimal) Phase 1 reicht **Uptime Kuma** auf demselben Marketing-VPS: ```yaml # /home/deploy/uptime-kuma/docker-compose.yml services: uptime: image: louislam/uptime-kuma:1 container_name: uptime restart: unless-stopped ports: - '127.0.0.1:3001:3001' volumes: - ./data:/app/data ``` Caddy-Block dafür: ```caddy status.digiformer.eu { reverse_proxy 127.0.0.1:3001 basicauth { pascal $2a$14$ # caddy hash-password generieren } } ``` Monitore einrichten: HTTPS-Check für jede Marken-Site + Forgejo. Bei Ausfall Mail an Pascal über Brevo SMTP. --- ## Disaster Recovery — Wenn der Marketing-VPS ausfällt Marketing-Site ist statisch und reproduzierbar: 1. **Neuer VPS bestellen** (Hetzner UI, ~5 min) 2. Server härten + Caddy installieren (Phase B.2 + B.3, ~20 min) 3. DNS A-Records auf neue IP umstellen (Hetzner DNS oder Registrar, ~5 min) 4. Forgejo-Workflow manuell triggern (Forgejo-UI → Actions → Run workflow) für jede Marken-Site (~5 min Build + Deploy pro Site) **Total RTO: ~45 min.** Kein RPO-Datenverlust, weil Build-Output reproduzierbar ist. Wenn der **Forgejo-VPS** ausfällt: 1. Neuer VPS bestellen 2. Backup von Storage Box ziehen (`gitea dump`) 3. Forgejo-Container hochfahren mit dump-restore 4. Runner neu registrieren **Total RTO: ~1 h.** RPO: bis 24 h (letztes Backup) — aber lokale Git-Clones haben aktuelle Stände, falls nötig push-zurück. --- ## Kostenbilanz Tier-2-Setup | Posten | €/mo | |---|---| | Forgejo-VPS CX22 + Backups | ~5 | | Marketing-VPS CX22 + Backups | ~5 | | Hetzner Storage Box BX21 (1 TB) — geteilt mit anderen Backups | ~10 | | Domain-Registrar (INWX o.ä.) | ~1/Domain/Jahr | | **Total Marketing-Infra** | **~20/mo** | Das ist fix für **alle** Marketing-Sites — egal ob 1 oder 10 Marken. --- ## Was als Nächstes - [ ] Pascal: VPS bestellen (Forgejo + Marketing) - [ ] Phase A durcharbeiten — Forgejo + Runner laufen, TLS aktiv - [ ] Phase B durcharbeiten — Marketing-VPS bereit für rsync - [ ] Phase C durcharbeiten — `slimcore-website` deployt unter `https://slimcore.io/` - [ ] Smoke-Test, dann live - [ ] Andere Marken-Sites einzeln nach Phase D nachziehen - [ ] Cockpit-Migration als separater Plan später (Phase E)