- 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
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_keyauf Forgejo-ServerMARKETING_HOST: IP oder DNS des Marketing-VPSMARKETING_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-websiteist 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:
pushaufmain→ Test, Build, Deploy auf Marketing-VPS unterslimcore.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:
- Repo in Forgejo anlegen / migrieren
.forgejo/workflows/deploy.ymlaus diesem Repo kopieren,slimcore.iodurch passende Domain ersetzen- DNS A-Record auf Marketing-VPS-IP zeigen
- 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:
- Neuer VPS bestellen (Hetzner UI, ~5 min)
- Server härten + Caddy installieren (Phase B.2 + B.3, ~20 min)
- DNS A-Records auf neue IP umstellen (Hetzner DNS oder Registrar, ~5 min)
- 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:
- Neuer VPS bestellen
- Backup von Storage Box ziehen (
gitea dump) - Forgejo-Container hochfahren mit dump-restore
- 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-websitedeployt unterhttps://slimcore.io/ - Smoke-Test, dann live
- Andere Marken-Sites einzeln nach Phase D nachziehen
- Cockpit-Migration als separater Plan später (Phase E)