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

549 lines
15 KiB
Markdown

# 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@<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`:
```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://<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`:
```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 → <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
```bash
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`):
```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 "<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
```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 | `<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
```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$<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)