Skip to content

Deployment

Diese Seite erklaert, wie ein Theme-Build aussieht (Dev vs. Production), welche Deployment-Strategien moeglich sind und wie das Staging-to-Live-Deployment-System des Update Managers funktioniert. nginx-Beispielkonfiguration und typische Fallstricke inklusive.

Build-Kommandos

Jedes Theme (_theme/vue-base/, _theme/projekt01/, ...) hat ein eigenes package.json mit Standard-Nuxt-Scripts.

Einmaliges Setup:

bash
cd _theme/vue-base
npm install

Die vier npm-Scripts:

ScriptZweck
npm run devDev-Server auf http://localhost:3000 mit Hot-Reload
npm run buildSSR-Production-Build nach .output/
npm run generateStatischer Build (SSG) nach .output/public/
npm run previewPreview des Production-Builds

Jedes Theme-Verzeichnis hat eigene node_modules/ und eigenen Build-Output — kein globales npm install im Repo-Root.

build vs. generate — SSR vs. SSG

KommandoOutputWann?
npm run build.output/ (Node-Server + Client-Bundle)SSR-Deployment, braucht Node.js-Laufzeit beim Hosting
npm run generate.output/public/ (nur statische HTML + Assets)SSG-Deployment, nur statischer Webserver noetig (nginx, Apache, CDN)

Im CMS kommt meist npm run build zum Einsatz — das Nuxt-Theme laeuft als Node-Prozess und holt Content dynamisch ueber /api/pages. Bei reiner statischer Auslieferung waere generate moeglich, aber der Pagebuilder-Content und Session-abhaengige Widgets wuerden dann beim Build eingefroren.

Output-Struktur nach npm run build

_theme/vue-base/.output/
├── nitro.json                  # Nitro-Server-Meta
├── public/                     # Statische Assets (client-side)
│   ├── _nuxt/                  # Gebundelte JS + CSS
│   │   ├── entry.*.css
│   │   └── *.woff2
│   ├── backend/                # Admin-CSS/JS (Copy aus public/)
│   ├── favicon.ico
│   └── robots.txt
└── server/                     # Nitro-Server (SSR)
    ├── index.mjs               # Entry-Point
    └── chunks/

.output/server/index.mjs ist der Einstieg fuer Production: node .output/server/index.mjs startet einen Listen-Port (default 3000).

Plesk-Deploy-Beispiel (deploy.sh)

Das Repo liefert unter _theme/vue-base/deploy.sh ein reales Plesk-Deploy-Skript mit:

bash
#!/bin/bash
export PATH=/opt/plesk/node/25/bin:$PATH

cd /pfad/zum/theme/_theme/vue-base

echo "Building Nuxt..."
npm run build >> build.log 2>&1

# Font + CSS-Dateinamen in JSON speichern (fuer Admin-CSS-Isolation)
ls .output/public/_nuxt/*.woff2 .output/public/_nuxt/entry.*.css 2>/dev/null | \
    xargs -I{} basename {} | \
    python3 -c "..."

echo "Restarting App..."
mkdir -p tmp
touch tmp/restart.txt   # Plesk-Passenger-Restart-Trigger

echo "Done!"

Die Besonderheit: fonts-manifest.json wird nach dem Build generiert — enthaelt die Hash-Dateinamen aus _nuxt/ fuer das Admin-CSS-Isolations-Plugin (admin-css.client.ts). Ohne dieses Manifest laedt das Admin-Layout keine Fonts.

Nuxt-Server hinter nginx

Fuer Production wird das Nuxt-Theme typisch hinter nginx mit Reverse-Proxy zum Node-Prozess betrieben.

Beispiel /etc/nginx/sites-available/example.com

nginx
# SSL-Redirect
server {
    listen 80;
    server_name www.example.com example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name www.example.com example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Nuxt-Frontend (Node-Prozess auf Port 3000)
    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;
        proxy_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        "upgrade";
    }

    # PHP-Backend fuer /api/* und /admin/* (inkl. Pfade ohne Trailing-Slash)
    location ~ ^/(api|admin)(/|$) {
        proxy_pass http://127.0.0.1:8080;   # PHP-Apache/FPM
        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;
    }

    # Upload-Ordner direkt ausliefern (Performance)
    # Der echte Upload-Pfad haengt von der Installation ab — typisch /z_uploads/
    # oder ein eigener Upload-Root. Die URLs kommen aus files::loadURL() serverseitig.
    location ~ ^/z_uploads/ {
        root /pfad/zum/cms;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}

X-Forwarded-For muss durchgereicht werden

Der PHP-Backend nutzt $_SERVER['HTTP_X_FORWARDED_FOR'] fuer IP-basiertes Rate-Limiting, Session-Binding und Audit-Logging. Ohne proxy_set_header X-Forwarded-For sehen alle Backend-Operationen nur die nginx-lokale IP (127.0.0.1) — Rate-Limits wirken dann gegen den Proxy, nicht gegen echte Besucher.

Node-Prozess beim Boot starten

Mit systemd (Debian/Ubuntu):

ini
# /etc/systemd/system/newmeta-frontend.service
[Unit]
Description=newmeta Nuxt Frontend
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/pfad/zum/cms/_theme/vue-base
ExecStart=/usr/bin/node .output/server/index.mjs
Environment=NUXT_API_KEY=...
Environment=NUXT_PUBLIC_API_BASE=https://www.example.com/api
Environment=NUXT_PUBLIC_SITE_URL=https://www.example.com
Environment=NUXT_PUBLIC_THEME=vue-base
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Aktivieren:

bash
systemctl daemon-reload            # einmal nach Anlegen/Aendern der Unit
systemctl enable newmeta-frontend
systemctl start  newmeta-frontend
systemctl status newmeta-frontend

Spaeter bei Aenderungen an der .service-Datei nochmal systemctl daemon-reload gefolgt von systemctl restart newmeta-frontend — ohne Daemon-Reload liest systemd die alte Config.

Mit Plesk: keine systemd-Unit noetig — Plesk-Passenger uebernimmt den Node-Prozess und reagiert auf tmp/restart.txt als Reload-Trigger.

Cache-Rebuild als Deploy-Step

Nach jedem Theme-Build muss zusaetzlich der PHP-CSS-Cache neu generiert werden, damit neue Design-Tokens, Custom-CSS und plugin-registry.js wirksam werden. Primaerer Weg: Admin → /admin/cache → "Cache leeren" manuell klicken. Hinter dem Button steht apcu_clear_cache() im Script cache_settings/script/cache_clear.php plus Cache-Rebuild via cache_compress.php.

Fuer automatisierte Deploy-Pipelines laesst sich der entsprechende Backend-Endpoint direkt via API-Key ansprechen — der exakte Pfad ist installationsabhaengig und im API-Key-Panel abrufbar.

Was genau passiert, haengt davon ab, was geaendert wurde:

  • Nur Theme-Code (Vue, Styles): npm run build + systemctl restart newmeta-frontend reicht
  • Neues Widget oder Custom-Override: zusaetzlich Cache-Rebuild, damit plugin-registry.js neu generiert wird
  • Design-Tokens-Aenderung via /admin/design: Core triggert Rebuild automatisch nach Save

Details: Nuxt-Aliases › Wann wird plugin-registry.js neu gebaut?.

Staging-to-Live via Update Manager

Das CMS bringt einen eigenen Staging-to-Live-Deployment-Mechanismus mit — unter /admin/update (Bereich "Deployments") lassen sich Ziel-Umgebungen konfigurieren und per Klick ausrollen.

Konzept

[Staging-Server]  ──────→  [Production-Server]
                   Deploy

1. ZIP-Archiv des Codes erstellen (exclude .git, node_modules, .nuxt, .output, .claude)
2. MySQL-Dump der Staging-DB erstellen
3. Dateien ins Ziel-Verzeichnis entpacken (mit Backup von config.local.php)
4. Tabellen im Ziel droppen + aus Dump importieren
5. URL-Replace: staging.example.com → www.example.com
   (serialized-aware: unserialize → recursive replace → serialize)
6. Webhook `deployment.completed` dispatchen

Konfiguration

Pro Ziel-Umgebung ein Eintrag in deployment_targets mit:

  • DB-Credentials (AES-256-CBC verschluesselt)
  • Ziel-Verzeichnis
  • Search/Replace-URL-Paar
  • Aktiv-Flag

Details in der Admin-UI /admin/update → "Deployments".

7-Schritte-Ablauf

  1. ZIP erstellen — aus dem aktuellen Code
  2. DB-Dumpmysqldump oder mariadb-dump (Auto-Detection)
  3. Files kopierenconfig.local.php des Zielsystems bleibt erhalten
  4. Ziel-DB leeren — alle Tabellen droppen
  5. DB importieren — Dump einspielen
  6. URL-Replace + Ident — serialized-aware, JSON-escaped, mit updateWebsiteIdent()
  7. Finalize — Status-Update, Webhook-Dispatch, Log-Eintrag

Ausschluesse

Tabellen wie Sessions, Logs, Queue, Cache werden in deployment_targets/config/deploy_exclude_tables.json vom Dump ausgeschlossen — ausser die Ziel-DB ist leer (Erst-Deploy), dann werden alle Tabellen eingespielt.

Nicht im Scope

Das Staging-to-Live-System kuemmert sich um PHP-Code + DB + Media. Das Nuxt-Theme muss separat deployed werden (npm run build auf dem Ziel-Server + App-Restart). Typischer Ablauf:

1. Auf Staging: Pagebuilder-Seiten publishen, testen
2. In Admin: /admin/update → Deployments → Ziel waehlen → "Deploy"
   → PHP-Code + DB + Media landen auf Production
3. Auf Production-Server: cd _theme/vue-base && ./deploy.sh
   → Nuxt-Build + App-Restart

Haeufige Fehler

X-Forwarded-For-Header fehlt im nginx-Proxy

Backend sieht nur 127.0.0.1 als IP → Rate-Limiter greift global, Session-Binding bricht, IP-Audit-Log ist nutzlos. Immer proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for im nginx-Config setzen.

Nuxt-Build mit falscher NUXT_PUBLIC_API_BASE

Die Env-Variable wird zum Build-Zeitpunkt ins Client-Bundle gebacken. Production-Build mit Staging-URL fuehrt zu Mixed-Content-Fehlern oder falschen API-Calls. .env vor npm run build pruefen, oder Build-Env explizit setzen: NUXT_PUBLIC_API_BASE=https://www.example.com/api npm run build.

App-Restart nach Build vergessen

npm run build aktualisiert .output/, aber der laufende Node-Prozess haelt weiter den alten Code. systemctl restart newmeta-frontend (oder touch tmp/restart.txt bei Plesk) ist Pflicht nach jedem Build.

Cache-Rebuild nach Widget-Aenderung vergessen

Neue widgets/*/template/index.vue-Datei → plugin-registry.js braucht Neugenerierung. Ohne Cache-Rebuild zeigt der Pagebuilder-Picker das Widget, aber das Rendering wirft Unknown widget type — weil die Registry das widget_template nicht kennt.

Deploy waehrend des Staging-Tests

Das Staging-to-Live loescht alle Tabellen im Ziel und ersetzt durch Staging-Dump. Wenn Production-User parallel Daten eingeben, gehen die verloren. Deploy immer in Wartungsfenstern oder nach explizitem Freeze auf Production.

deploy.sh mit falschen Pfaden

Das reale deploy.sh im Repo hat hart-codierte Pfade (/var/www/vhosts/...). Fuer andere Hosts unbedingt anpassen — sonst schreibt der Build in Fremd-Verzeichnisse oder wirft cd-Fehler.

Siehe auch