MADORIMADORI

Deployment

Madori runs anywhere Node.js runs. All content is stored as flat files — no database required. This makes deployment straightforward: build the Next.js application and serve it from any hosting environment that provides a persistent filesystem.

For full Control Panel functionality (content editing, asset uploads, user management), you need a hosting environment with a writable filesystem. Serverless platforms work for read-only frontends where content is committed to Git.


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
HOSTNAME No 0.0.0.0 Bind address for the server
DISABLE_CP No Set to true to disable the Control Panel in production

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

System Requirements

Requirement Minimum Recommended
Node.js 18+ 20 LTS
RAM 256 MB 512 MB+
Disk Project size + assets SSD for responsive CP
pnpm 8+ Latest

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

Persistent filesystem with always-on processes. Full Control Panel support without VPS management.


Common Patterns

Content in Git

Since all content is flat files, commit content to your repository:

git add content/
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: true, introspection: false },
}

The frontend reads content from the GraphQL API while the CP runs on a separate server with filesystem access.

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 GraphQL endpoint as a health check:

curl -f http://localhost:3000/api/graphql?query={__typename}

Returns 200 if the server is healthy.

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/