Deployment
This page covers how a theme build works (dev vs. production), which deployment strategies are possible, and how the staging-to-live deployment system of the Update Manager works. nginx example configuration and typical pitfalls included.
Build commands
Every theme (_theme/vue-base/, _theme/projekt01/, …) has its own package.json with standard Nuxt scripts.
One-time setup:
cd _theme/vue-base
npm installThe four npm scripts:
| Script | Purpose |
|---|---|
npm run dev | Dev server on http://localhost:3000 with hot reload |
npm run build | SSR production build into .output/ |
npm run generate | Static build (SSG) into .output/public/ |
npm run preview | Preview of the production build |
Each theme directory has its own node_modules/ and its own build output — no global npm install at the repo root.
build vs. generate — SSR vs. SSG
| Command | Output | When? |
|---|---|---|
npm run build | .output/ (Node server + client bundle) | SSR deployment, needs a Node.js runtime on the host |
npm run generate | .output/public/ (static HTML + assets only) | SSG deployment, only a static web server required (nginx, Apache, CDN) |
In the CMS, npm run build is usually the right choice — the Nuxt theme runs as a Node process and fetches content dynamically via /api/pages. Pure static delivery would be possible with generate, but Pagebuilder content and session-dependent widgets would then be frozen at build time.
Output structure after npm run build
_theme/vue-base/.output/
├── nitro.json # Nitro server meta
├── public/ # Static assets (client-side)
│ ├── _nuxt/ # Bundled JS + CSS
│ │ ├── entry.*.css
│ │ └── *.woff2
│ ├── backend/ # Admin CSS/JS (copied from public/)
│ ├── favicon.ico
│ └── robots.txt
└── server/ # Nitro server (SSR)
├── index.mjs # Entry point
└── chunks/.output/server/index.mjs is the entry point for production: node .output/server/index.mjs starts a listen port (default 3000).
Plesk deploy example (deploy.sh)
The repo ships a real Plesk deploy script under _theme/vue-base/deploy.sh:
#!/bin/bash
export PATH=/opt/plesk/node/25/bin:$PATH
cd /path/to/theme/_theme/vue-base
echo "Building Nuxt..."
npm run build >> build.log 2>&1
# Save font + CSS file names into JSON (for 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!"The detail to watch: fonts-manifest.json is generated after the build — it contains the hashed file names from _nuxt/ for the admin CSS isolation plugin (admin-css.client.ts). Without this manifest, the admin layout loads no fonts.
Nuxt server behind nginx
In production, the Nuxt theme is typically run behind nginx as a reverse proxy to the Node process.
Example /etc/nginx/sites-available/example.com
# 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 process on 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 for /api/* and /admin/* (incl. paths without a 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;
}
# Serve the upload folder directly (for performance)
# The actual upload path depends on the install — typically /z_uploads/
# or a dedicated upload root. The URLs are produced server-side by files::loadURL().
location ~ ^/z_uploads/ {
root /path/to/cms;
expires 30d;
add_header Cache-Control "public, immutable";
}
}X-Forwarded-For must be forwarded
The PHP backend uses $_SERVER['HTTP_X_FORWARDED_FOR'] for IP-based rate limiting, session binding, and audit logging. Without proxy_set_header X-Forwarded-For, every backend operation only sees the local nginx IP (127.0.0.1) — rate limits then hit the proxy rather than real visitors.
Start the Node process at boot
With systemd (Debian/Ubuntu):
# /etc/systemd/system/newmeta-frontend.service
[Unit]
Description=newmeta Nuxt Frontend
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/path/to/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.targetActivate:
systemctl daemon-reload # once after creating/changing the unit
systemctl enable newmeta-frontend
systemctl start newmeta-frontend
systemctl status newmeta-frontendAfter changes to the .service file, run systemctl daemon-reload followed by systemctl restart newmeta-frontend — without the daemon reload, systemd keeps reading the old config.
With Plesk: no systemd unit required — Plesk Passenger manages the Node process and reacts to tmp/restart.txt as a reload trigger.
Cache rebuild as a deploy step
After every theme build, you additionally have to regenerate the PHP CSS cache so new design tokens, custom CSS, and plugin-registry.js take effect. Primary path: admin → /admin/cache → click "Clear cache" manually. Behind the button sits apcu_clear_cache() in the script cache_settings/script/cache_clear.php, plus the cache rebuild via cache_compress.php.
For automated deploy pipelines, the corresponding backend endpoint can be called directly with an API key — the exact path is install-dependent and visible in the API key panel.
What exactly happens depends on what changed:
- Theme code only (Vue, styles):
npm run build+systemctl restart newmeta-frontendis enough - New widget or custom override: additionally trigger the cache rebuild so
plugin-registry.jsis regenerated - Design-token change via
/admin/design: the core triggers the rebuild automatically on save
Details: Nuxt aliases › When does plugin-registry.js get rebuilt?.
Staging to live via the Update Manager
The CMS ships its own staging-to-live deployment mechanism — under /admin/update (the "Deployments" area) you can configure target environments and roll out with a click.
Concept
[Staging server] ──────→ [Production server]
Deploy
1. Build a ZIP of the code (excluding .git, node_modules, .nuxt, .output, .claude)
2. Take a MySQL dump of the staging DB
3. Extract files into the target directory (with a backup of config.local.php)
4. Drop the target's tables + import the dump
5. URL replace: staging.example.com → www.example.com
(serialized-aware: unserialize → recursive replace → serialize)
6. Dispatch the `deployment.completed` webhookConfiguration
One entry per target environment in deployment_targets, with:
- DB credentials (AES-256-CBC encrypted)
- Target directory
- Search/replace URL pair
- Active flag
Details in the admin UI /admin/update → "Deployments".
7-step flow
- Create ZIP — from the current code
- DB dump —
mysqldumpormariadb-dump(auto-detection) - Copy files — the target system's
config.local.phpis preserved - Clear target DB — drop every table
- Import DB — apply the dump
- URL replace + ident — serialized-aware, JSON-escaped, with
updateWebsiteIdent() - Finalize — status update, webhook dispatch, log entry
Exclusions
Tables like sessions, logs, queue, and cache are excluded from the dump via deployment_targets/config/deploy_exclude_tables.json — unless the target DB is empty (first-time deploy), in which case every table is imported.
Out of scope
The staging-to-live system handles PHP code + DB + media. The Nuxt theme has to be deployed separately (npm run build on the target server + app restart). Typical flow:
1. On staging: publish Pagebuilder pages, run tests
2. In the admin: /admin/update → Deployments → pick target → "Deploy"
→ PHP code + DB + media land on production
3. On the production server: cd _theme/vue-base && ./deploy.sh
→ Nuxt build + app restartCommon issues
X-Forwarded-For header missing from the nginx proxy
The backend only sees 127.0.0.1 as the IP → the rate limiter hits globally, session binding breaks, the IP audit log is useless. Always set proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for in the nginx config.
Nuxt build with the wrong NUXT_PUBLIC_API_BASE
The env variable is baked into the client bundle at build time. A production build with a staging URL causes mixed-content errors or wrong API calls. Check .env before npm run build, or set the build env explicitly: NUXT_PUBLIC_API_BASE=https://www.example.com/api npm run build.
App restart missing after the build
npm run build updates .output/, but the running Node process keeps serving the old code. systemctl restart newmeta-frontend (or touch tmp/restart.txt on Plesk) is mandatory after every build.
Cache rebuild missing after a widget change
A new widgets/*/template/index.vue → plugin-registry.js has to be regenerated. Without a cache rebuild, the Pagebuilder picker shows the widget, but rendering throws Unknown widget type — because the registry doesn't know the widget_template.
Deploy during a staging test
Staging-to-live drops every table in the target and replaces them with the staging dump. If production users are entering data in parallel, that data is lost. Always deploy during maintenance windows or after an explicit freeze on production.
deploy.sh with wrong paths
The real deploy.sh in the repo has hard-coded paths (/var/www/vhosts/...). For other hosts, absolutely adapt them — otherwise the build writes into foreign directories or throws cd errors.
See also
- Theme structure —
package.json+ scripts,.output/layout - Environment variables — set production env before the build
- Design tokens › Cache integration — when the cache rebuild is required
- Nuxt aliases —
plugin-registry.jsand auto-generation _theme/vue-base/deploy.sh— real Plesk deploy script in the repo_public/extensions/core/backend/updatemanager/— the staging-to-live system