Was ist Headless CMS — und warum eigenes Hosting?
Ein Headless CMS trennt das Backend (Content-Speicherung, Editor-Oberfläche) vom Frontend (die Website). Das CMS liefert Content über eine API (REST oder GraphQL) aus; das Frontend holt ihn sich und rendert ihn. Das gibt dir maximale Freiheit beim Frontend-Stack: Next.js, Gatsby, Hugo, Astro — whatever.
Die Optionen beim Hosting:
- Fully Managed (SaaS): Contentful, Sanity, Strapi Cloud — Hosting inklusive, aber du hast keine Kontrolle über die Infrastruktur und zahlst nach Volume (API-Calls, Assets)
- Self-hosted CMS: Strapi, Ghost, Directus, oder ein selbstgebautes — du hostest das CMS selbst und hast volle Kontrolle
- Headless-Frontend (SSG/SSR): Next.js, Gatsby, Hugo, Astro auf eigenem Server oder bei einem CDN-Provider
Eigenes Hosting lohnt sich, wenn du Kosten sparen willst (keine SaaS-Gebühren), volle Kontrolle über Daten und Compliance brauchst (DSGVO, Ruhrgebiets-DSGVO-konform), oder komplexe Custom-Logik im Frontend brauchst, die ein Managed-Service nicht abdeckt.
Option 1: Statische Seiten mit Next.js (Static Export)
Der einfachste Fall: Next.js im Static-Export-Modus. Kein Server, keine Node-Runtime auf dem Host — nur HTML, CSS, JS und ein CDN. Perfekt für Blogs, Marketing-Sites, Dokumentation.
# package.json — next export enabled
{
"scripts": {
"export": "next build && next export"
}
}
# next.config.js
module.exports = {
output: 'export',
images: { unoptimized: true }
}
Static Export produziert ein /out-Verzeichnis mit reinen HTML-Dateien. Das kannst du auf jedem static-hosting-Provider deployen: Nginx, Apache, S3 + CloudFront, oder ein simpler VPS mit nginx.
Static Export mit Nginx ausliefern
# /etc/nginx/sites-available/headless-static
server {
listen 80;
server_name blog.deine-domain.de;
root /var/www/nextjs-site/out;
index index.html;
# Alle Routes auf index.html (Next.js Client-Side Routing)
location / {
try_files $uri $uri/ $uri.html /index.html;
}
# Cache-Control für statische Assets
location ~* \/_next\/static\/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Bilder und Fonts
location ~* \/(public|static)\/ {
expires 6M;
add_header Cache-Control "public";
}
# GZIP
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript;
}
CDN-Tipp für statische Sites
Statische Next.js-Sites auf einem eigenen VPS ausliefern ist gut — aber ein CDN davor (Cloudflare, Bunny.net, AWS CloudFront) macht sie global performant. Bei Bunny.net kostet ein TB Traffic ca. €0.01. Die Site liegt einmal auf dem VPS und wird auf alle 90+ Peering-Punkte distributed.
Option 2: Next.js mit SSR (Node.js Server)
Wenn du SSG nicht nutzen kannst (personalisiierter Content, A/B-Tests, Auth-geschützte Seiten), brauchst du einen Node.js-Server. Next.js mit dem eingebauten Server starten:
# package.json
{
"scripts": {
"start": "next start -p 3000",
"build": "next build"
}
}
# Server starten
npm start
# Oder mit PM2 (Production Process Manager)
pm2 start npm --name "nextjs-app" -- start
pm2 save
Der Server lauscht auf Port 3000. Nginx davor als Reverse Proxy:
# /etc/nginx/sites-available/nextjs-ssr
server {
listen 80;
server_name app.deine-domain.de;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket Support (für Next.js HMR in Dev)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeouts
proxy_connect_timeout 60s;
proxy_read_timeout 60s;
}
}
PM2-Konfiguration für Production
# ecosystem.config.js
module.exports = {
apps: [{
name: 'nextjs-app',
script: 'node_modules/.bin/next',
args: 'start -p 3000',
cwd: '/var/www/my-nextjs-app',
instances: 2,
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000
},
max_memory_restart: '500M',
error_file: '/var/log/pm2/nextjs-error.log',
out_file: '/var/log/pm2/nextjs-out.log',
time: true
}]
};
# Starten:
pm2 start ecosystem.config.js
pm2 startup # Autostart bei Server-Reboot
Option 3: Gatsby / Hugo auf VPS
Gatsby und Hugo folgen einem anderen Modell: sie sind Static Site Generators. Du baust die Site (local oder in CI/CD) und deployst das Output. Kein Node.js zur Laufzeit nötig.
# Gatsby Build (lokal oder in CI)
npm install
npm run build
# Output in /public
# Auf VPS: nur Nginx oder Apache
# Gatsby produziert rein statisches HTML + JS
# Alles im /public-Verzeichnis
# Hugo noch simpler: ein Binary, keine Node.js
wget https://github.com/gohugoio/hugo/releases/download/v0.123.0/hugo_extended_0.123.0_linux-amd64.tar.gz
tar -xzf hugo_extended_*.tar.gz
./hugo
# Output: /public — deploy to nginx
Option 4: Self-hosted CMS (Strapi oder Directus)
Für das Backend — das CMS selbst — ist Strapi eine der populärsten Open-Source-Optionen. Es läuft als Node.js-App mit PostgreSQL oder SQLite als Datenbank:
# Strapi Projekt erstellen
npx create-strapi-app@latest mein-cms --quickstart
# Oder mit PostgreSQL:
npx create-strapi-app@latest mein-cms --dbclient=postgres --dbhost=localhost --dbport=5432 --dbname=strapi --dbusername=strapi --dbpassword=mein-passwort
# Strapi mit PM2 in Production
pm2 start node_modules/.bin/strapi -- start --name "strapi-app"
Strapi hat eine eingebaute Admin-Oberfläche unter /admin. Für Production: SSL vor dem Strapi-Server, da Strapi keine SSL-Terminierung macht (Nginx davor).
Strapi + Nginx Reverse Proxy
server {
listen 443 ssl http2;
server_name cms.deine-domain.de;
ssl_certificate /etc/letsencrypt/live/cms.deine-domain.de/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cms.deine-domain.de/privkey.pem;
location / {
proxy_pass http://127.0.0.1:1337;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket für Strapi Upload
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
CI/CD Pipeline: Automatischer Build bei Content-Änderung
Das ist der eigentliche Vorteil von Headless CMS: wenn ein Redakteur im CMS speichert, soll automatisch ein Rebuild der Site starten. Das braucht einen Webhook und einen CI/CD-Prozess.
GitHub Actions für automatische Gatsby/Next.js Builds
# .github/workflows/deploy.yml
name: Deploy Headless Site
on:
push:
branches: [main]
repository_dispatch:
types: [CMS_CONTENT_UPDATED] # Webhook trigger
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install and Build
run: |
npm ci
npm run build
- name: Deploy to Server
uses: appleboy/scp-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: deploy
key: ${{ secrets.SERVER_SSH_KEY }}
source: "./out/*" # Static export output
target: "/var/www/static-site/out/"
strip_components: 1
- name: Run Rsync (alternative)
run: |
rsync -avz --delete out/ deploy@server:/var/www/static-site/out/
env:
RSYNC_PASSWORD: ${{ secrets.DEPLOY_PASSWORD }}
Strapi Webhook → GitHub Dispatch
In Strapi: Settings → Webhooks → neuer Webhook:
- URL:
https://api.github.com/repos/dein-user/dein-repo/dispatches - Headers:
Authorization: token GH_TOKEN,Accept: application/vnd.github.v3+json - Events: entry.create, entry.update, entry.delete, media.create, media.update
Contentful / Sanity Webhooks
Bei Contentful und Sanity (Managed CMS, aber du hostest das Frontend) gehst du genauso vor: CMS → Webhook → GitHub Actions → Build → Deploy. Die Webhook-URL zeigen auf deinen GitHub Repository Dispatch Endpoint. Ein persönlicher Access Token als Secret macht das sicher.
Caching-Strategie für Headless Sites
Headless CMS mit statischen Seiten hat einen entscheidenden Vorteil: aggressives Caching. Die Seite ändert sich nur beim Rebuild — dazwischen ist sie statisch und kann ewig gecacht werden.
| Content-Typ | Cache-Dauer | Invalidierung | Methode |
|---|---|---|---|
| Blog-Post (unverändert) | 1 Jahr | Never (oder auf Rewrite) | Cache-Control: public, max-age=31536000, immutable |
| Blog-Post (frisch veröffentlicht) | 5 min | Nach Rebuild + CDN Purge | Tag-basiertes CDN Purge bei Webhook |
| Media (Bilder, PDFs) | 1 Jahr | Dateiname mit Hash | /static/images/file.abc123.png |
| API-Responses (Headless CMS) | 60 Sekunden | TTL-basiert | Cache-Control: public, max-age=60 |
Performance-Benchmark: Static vs. SSR
Die Wahl zwischen Static Generation und Server-Side Rendering hat messbare Performance-Unterschiede:
# Static (Gatsby mit CDN):
# TTFB: ~10-50ms (CDN Edge Node)
# FCP: ~0.8s (vorgerenderte HTML)
# LCP: ~1.2s (Bilder optimiert)
# Lighthouse: 95-100
# SSR (Next.js mit Node.js Server):
# TTFB: ~80-200ms (Node.js Rendering + DB-Query)
# FCP: ~1.5s
# LCP: ~2.0s
# Lighthouse: 75-90
# ISR (Next.js Incremental Static Regeneration):
# TTFB: ~20-80ms (stale-while-revalidate)
# FCP: ~1.0s
# Lighthouse: 85-95
ISR (Incremental Static Regeneration) ist ein guter Kompromiss: statische Seiten, die im Hintergrund refreshed werden, ohne dass der ganze Build neu läuft. Next.js ISR erlaubt revalidate: 60 — die Seite ist stale, wird aber sofort ausgeliefert, während ein Background-Refresh läuft.
Fazit: Headless ist kein Overkill
Headless CMS auf eigenem Hosting ist nicht komplizierter als WordPress — es ist nur anders. Der Build-Prozess ist der neue "Update-Zyklus", Webhooks sind der neue "Auto-Update". Sobald die CI/CD-Pipeline steht, ist es sogar wartungsärmer als ein Managed CMS, weil du keine Vendor-Lock-in hast und keine Platform-Gebühren zahlst.
Die richtige Architektur:
- Einfache Sites (Blog, Portfolio): Hugo oder Gatsby + static export + Nginx + Cloudflare — minimaler Aufwand, maximale Performance
- Komplexe Sites mit Auth/Personalisierung: Next.js mit SSR + PM2 Cluster + Nginx — mehr Ops-Aufwand, aber volle Kontrolle
- Self-hosted CMS: Strapi + PostgreSQL + Nginx — wenn du kein Managed-SaaS willst
Die Kosten für eigenes Headless-Hosting liegen bei €5–20/Monat für einen VPS. Das ist ein Bruchteil dessen, was Contentful oder Sanity für vergleichbare Nutzung kosten.
VPS für Headless CMS-Projekte
Node.js-fähiger Hosting mit SSH-Zugang, git-Deploy und 99,9% Uptime — ideal für Next.js, Gatsby und Strapi.
Zum Hosting-Vergleich