MADORIMADORI

Deployment

Madori's writable CMS runs on a Node.js server with persistent storage. Content is stored as flat files; no database is required. Build the Next.js application and preserve writable data across application releases.

For Control Panel functionality (content editing, asset uploads, user management), run one application process per writable project storage location. File locks and cache generations are local to that process; multiple writers are not supported by the bundled storage implementation. A separate read-only frontend can use bundled content or a remote content API, with the integration described below.


Configuration Reference

Environment Variables

Variable Required Default Description
NODE_ENV No development Set to production for secure cookies, disabled introspection, and optimised builds
PORT No 3000 Port for the Node.js server

Use cp.enabled: false in madori.config.ts for a frontend-only deployment. Pass an explicit bind address to Next with pnpm start -H 0.0.0.0 when required.

Build Commands

Command Description
pnpm build Build the Next.js application for production
pnpm start Start the production server
pnpm dev Start the development server with hot reload

madori.config.ts (Deployment-Relevant Options)

Option Type Default Description
graphql.introspection boolean true in dev Set to false in production to hide schema from public inspection
cp.enabled boolean true Disable the CP if deploying frontend-only
auth.storeConfig.sessionDurationMs number 86400000 Session expiry — consider shortening for production
sites SiteDefinitionConfig[] one local default Public origins/locales used for canonical URLs and host routing
seo.operationalStoragePath string ./storage/seo Writable operational storage for 404 observations and report snapshots
seo.errorTracking boolean true Enable bounded, normalized public 404 observation recording
seo.redirects boolean true Enable authored redirects and public redirect execution
seo.reports boolean true Enable audit report generation and report API
staticCache.enabled boolean false Enable bounded public HTML caching; requires writable cache storage and matching configured site origin

System Requirements

Requirement Minimum Recommended
Node.js 22+ Current supported LTS
RAM Depends on workload Size for content, build, and asset workload
Disk Project size + assets SSD for responsive CP
pnpm Project package-manager version Latest compatible version

Usage Examples

Basic Production Build

pnpm build
pnpm start

This starts a production Next.js server on port 3000.

VPS Deployment (Recommended for Full CP)

A VPS provides a persistent filesystem, meaning the Control Panel works fully — content editing, asset uploads, and user management all persist between deploys.

Providers: DigitalOcean, Hetzner, Vultr, Linode, AWS EC2

Deploy script:

git pull origin main
pnpm install --frozen-lockfile
pnpm build
# Restart your process manager
pm2 restart madori

Nginx Reverse Proxy

Run Next.js on a private local port and point Nginx to that exact port. SSL can terminate at Nginx or Cloudflare; Madori does not need an internal callback URL for Control Panel authentication.

pnpm start -p 3001
server {
    listen 80;
    server_name yoursite.com;

    location / {
        proxy_pass http://127.0.0.1:3001;
        proxy_http_version 1.1;

        proxy_buffer_size 16k;
        proxy_buffers 8 16k;
        proxy_busy_buffers_size 32k;

        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        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_cache_bypass $http_upgrade;
    }
}

The Control Panel Proxy only checks for the madori_session cookie before rendering. Protected API handlers perform authoritative session validation. This avoids a request from the Next.js Proxy back into the same server and works with custom ports, SSL termination, and Cloudflare proxying without extra environment variables.

Process Management with systemd

[Unit]
Description=Madori CMS
After=network.target

[Service]
Type=simple
User=deploy
WorkingDirectory=/var/www/my-site
ExecStart=/usr/bin/pnpm start -p 3001
Restart=on-failure
Environment=NODE_ENV=production

[Install]
WantedBy=multi-user.target

Process Management with PM2

pm2 start pnpm --name madori -- start -p 3001
pm2 save
pm2 startup

Keep PM2 and Nginx ports identical. If PM2 starts Madori on 3001, proxy_pass must use http://127.0.0.1:3001.

Vercel / Netlify (Frontend Only)

Serverless platforms work for the frontend site but the Control Panel won't persist changes — the filesystem is read-only and ephemeral.

Good for: marketing sites, documentation, and blogs where content is committed to Git.

Railway / Render

Configure an always-on service with an attached persistent volume and one application instance. Map every writable data path to that volume; do not assume the service's default deployment filesystem persists. Verify the storage and restart behavior of your chosen plan before enabling editorial writes.


Common Patterns

SEO Runtime Storage

SEO has two storage classes:

  • Versioned content: resources/seo/sites, resources/seo/sections, and content/seo/redirects. These files can live in the application repository or an explicitly tracked separate content repository.
  • Operational state: seo.operationalStoragePath stores 404 observations and report snapshots. Keep this directory writable, persistent, backed up, and outside Git sync paths. SEO resolution caching is in memory; persistent redirect-hit counters are not implemented.

Do not deploy a shared writable operational directory across unrelated sites. Give each site or deployment its own storage scope. Never expose this directory through static hosting.

Multi-Site Routing

Configure every public origin in sites. Domain sites are selected from the request host; a shared-host deployment may use a path prefix in its reverse proxy and still keeps canonical URLs scoped to the configured site. Set a trusted Host/forwarded-host policy at the edge and do not let clients select a site through arbitrary headers.

Public SEO routes are generated dynamically: /sitemap.xml, /robots.txt, and /humans.txt. Authored redirects run before page rendering for GET and HEAD requests. Control Panel and API paths are excluded from public redirect handling.

SEO in Serverless Deployments

Read-only metadata and sitemap rendering can run on serverless infrastructure when content is bundled or fetched from a stable source. 404 tracking, report snapshots, and CP writes require a persistent writable filesystem. Disable cp.enabled, seo.errorTracking, seo.reports, git.enabled, and staticCache.enabled when no durable writable storage is available. A custom frontend must supply its own remote data integration; the bundled file engine does not switch to remote storage automatically.

Content in Git

For automatic commits and GitHub pushes from the Control Panel, see Git Content Sync. Automatic sync needs persistent writable storage for content, each .git directory, and git.statePath; ephemeral serverless filesystems are not suitable.

Commit authored content to your repository, excluding private submissions and operational data. For example:

git add content/collections/blog/example-post.md
git commit -m "Update blog posts"
git push

This gives you:

  • Version history for all content changes
  • Pull request workflow for content review
  • Easy rollback if something goes wrong
  • Consistent content across dev/staging/production

Automated Deployment on Push

Use GitHub Actions or similar CI to deploy on push to main:

# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: ssh [email protected] "cd /var/www/my-site && git pull && pnpm install --frozen-lockfile && pnpm build && pm2 restart madori"

Separate Frontend and CP Deployments

Deploy the frontend to a CDN/serverless platform and the CP to a VPS:

// madori.config.ts on the frontend deployment
const config = {
  cp: { enabled: false },
  graphql: { enabled: false },
  seo: { errorTracking: false, reports: false },
  git: { enabled: false },
  staticCache: { enabled: false },
}

export default config

This disables the local Control Panel and write-dependent options; it does not connect the frontend to the CMS. Adapt frontend reads to the public published-entry API or use authenticated GraphQL from server-side code against the separate CMS origin. Keep GraphQL credentials server-side. If content is bundled instead, arrange a rebuild after publishing. See SDK and content access for transport and authentication choices.

SSL with Let's Encrypt

Use Certbot for free SSL certificates:

sudo certbot --nginx -d yoursite.com

Most server management tools (Ploi, Forge, Coolify) handle SSL with one click.

Troubleshooting Control Panel 502 Errors

If the marketing site works but /cp returns 502 Bad Gateway:

  1. Confirm PM2 is running the expected command and port:

    pm2 show madori
    pm2 logs madori
    
  2. Request the Control Panel directly from the server, bypassing Nginx and Cloudflare:

    curl -I http://127.0.0.1:3001/cp
    

    A 307 redirect to /cp/login without a session is expected.

  3. Confirm Nginx proxy_pass uses the same host and port as the PM2 process.

  4. Remove obsolete INTERNAL_URL configuration from older deployments. Current Madori versions do not make an internal session-validation HTTP request.

  5. Check the Nginx error log. upstream sent too big header while reading response header from upstream means the upstream response headers exceeded Nginx's buffer. Deploy the latest Madori build and ensure the location block includes:

    proxy_buffer_size 16k;
    proxy_buffers 8 16k;
    proxy_busy_buffers_size 32k;
    

    These values provide headroom for framework-generated headers and future application changes.

  6. Reload Nginx and restart Madori after configuration or build changes:

    sudo nginx -t
    sudo systemctl reload nginx
    pm2 restart madori
    

Health Check Endpoint

Use the dedicated liveness or readiness endpoint:

curl -f http://localhost:3000/api/health/live
curl -f http://localhost:3000/api/health/ready

These endpoints return 200 when their corresponding health check succeeds.

Asset Backup

Back up uploaded assets separately since they're not always in Git:

# Rsync assets to backup location
rsync -avz /var/www/my-site/public/assets/ /backups/assets/