slimcore-website/docs/deployment.md
Pascal Oelmann 3c79b63db5
Some checks failed
Deploy Marketing-Site / Lint + Smoke-Tests (push) Failing after 1m9s
Deploy Marketing-Site / Deploy auf Marketing-VPS (push) Failing after 0s
Deploy Marketing-Site / Deploy-Notification (push) Successful in 9s
Initial Astro-Build, Deployment-Setup und Forgejo-Workflow
- Astro 6 + React + Tailwind v4 Projekt-Skelett mit allen Marketing-Seiten
  (Home, Module, Tester, Souveränität, Roadmap, Kontakt, Impressum, Datenschutz)
- Self-hosted Outfit + JetBrains Mono Fonts (DSGVO)
- Marketing-Komponenten gemäss CLAUDE.md §5.6 (NumberedItem, ModuleCard,
  StatusDot, TechStrip, SovereigntyBlock, RoadmapTimeline, etc.)
- Module-Daten in src/content/module.ts als Single Source of Truth
- E2E Smoke-Tests via Playwright
- OG-Image-Generator
- Forgejo Workflow .forgejo/workflows/deploy.yml für Tier-2 Static Deploy
- Infra-as-Code Snapshot in infra/marketing-vps/
- Brand-System Submodule auf Forgejo umgezogen (war GitHub)
- Deployment- und Handoff-Dokumentation
- .DS_Store aus Tracking entfernt, .gitignore um Test-Artefakte ergaenzt
2026-05-05 01:59:35 +02:00

15 KiB

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)

# SSH als root
ssh root@<forge-ip>

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

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
# 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://<forge-ip>: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:

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:

forge.digiformer.eu {
  reverse_proxy 127.0.0.1:3000
  encode zstd gzip
  header Strict-Transport-Security "max-age=31536000"
}
docker compose up -d
# DNS A-Record forge.digiformer.eu → <forge-ip>
# 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
docker compose -f /home/deploy/forgejo/docker-compose.yml exec runner forgejo-runner register \
  --no-interactive \
  --instance https://forge.digiformer.eu \
  --token <runner-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):

# /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
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:

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:

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

# Auf Forgejo-Server (deploy-user)
ssh-keygen -t ed25519 -f ~/.ssh/marketing_deploy_key -N ""
cat ~/.ssh/marketing_deploy_key.pub
# Auf Marketing-VPS (deploy-user)
adduser --disabled-password --gecos "" rsync-deploy
mkdir -p /home/rsync-deploy/.ssh
echo "<public-key-vom-forgejo-server>" >> /home/rsync-deploy/.ssh/authorized_keys
# Restriktion: nur rsync, kein Shell-Login
echo 'command="rrsync -wo /var/www" <public-key>' > /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

# 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 <marketing-vps-ip>
www.slimcore.io A <marketing-vps-ip>
digiformer.eu (später) A <marketing-vps-ip>
forge.digiformer.eu A <forge-vps-ip>
marketing.digiformer.eu A <marketing-vps-ip> (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

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:

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:

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:

# /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:

status.digiformer.eu {
  reverse_proxy 127.0.0.1:3001
  basicauth {
    pascal $2a$14$<bcrypt-hash>  # 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)