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:

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.

<100ms
First Contentful Paint (SSG)
90+
Lighthouse Score (static)
60%
Kostenreduktion vs. Managed

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:

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:

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