CaveCMS
Get started

Five steps.
Twenty minutes.

By the time your tea is brewed, you have a real CaveCMS site running on your own server, under your own domain, with your own name on the door.

Still evaluating? See what’s inside on the features page, or check pricing first.

Your server
01

Prep your server.

A small Linux VPS with sudo. MariaDB or MySQL running. nginx or Apache running. Node 20+. PM2 and wget installed (the installer downloads the signed release with wget).

Hetzner CX22, DigitalOcean Basic, Linode Nanode: anything modern works. If you have an existing VPS with your other sites, you already have this.

02

Point your domain at the server.

Add these A records at your DNS provider so my-site.com (and www) resolves to your server's public IP. Do this BEFORE running the installer in step 04. The installer's signed CAVECMS_SITE_URL bakes the domain into env.production, and certbot in step 05 needs HTTP-01 to reach the box on port 80.

Add these DNS records

2 records

Replace 203.0.113.42 with your server's actual public IP. Every record below uses it as a placeholder.

TypeNameContentProxyTTL
Amy-site.com203.0.113.42DNS onlyAuto
Awww203.0.113.42DNS onlyAuto

Verify

dig +short my-site.com

→ 203.0.113.42 (your server's IP, keep going)

Replace 203.0.113.42 with your server's public IP. Behind Cloudflare? Leave the orange cloud OFF for the first install so certbot can hit port 80 directly, then flip it back on after the cert is provisioned.

03

Create a database for your site.

Drop into mysql as root and create a dedicated database + user. CaveCMS gets `ALL PRIVILEGES` on that one database only, with no superuser needed at runtime.

In your terminal

Edit the highlighted fields below before you copy. The copy button bakes your values into the snippet, so there is no fumbling at the terminal prompt to fix placeholders after paste.

CREATE DATABASE  CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER ''@'localhost' IDENTIFIED BY '';
GRANT ALL PRIVILEGES ON .* TO ''@'localhost';

Database and user default to the same name; edit either independently if you want them separate. Tap "Gen" for a base64-24 password, or type your own.

04

Run the installer.

One command downloads the latest signed release, verifies its signature, generates every secret, seals an env.production file (mode 600), runs the migrations, and starts the app under PM2. About ninety seconds.

In your terminal

Edit the highlighted fields below before you copy. The copy button bakes your values into the snippet, so there is no fumbling at the terminal prompt to fix placeholders after paste.

npx create-cavecms@latest  \
  --surface=pm2 --port= \
  --dir=/var/www/html//public_html

Also: --surface=systemd for distro-native, --surface=manual if you run your own process supervisor.

05

Add an nginx vhost.

Paste this block as /etc/nginx/conf.d/my-site.com.conf, then run the two commands. certbot rewrites the vhost in place to add HTTPS + the HTTP→HTTPS redirect.

/etc/nginx/conf.d/my-site.com.conf

server {
    listen 80;
    server_name my-site.com;
    client_max_body_size 25M;

    # CaveCMS (the Node app) already sends the security headers on every
    # PROXIED response, so adding them here too would send each one
    # twice. nginx quirk: a location with its own add_header does NOT
    # inherit these server-level ones — so the two disk-served locations
    # below (which never reach Node) re-apply X-Content-Type-Options
    # themselves. HSTS lives on the HTTPS server block certbot adds.
    server_tokens off;
    proxy_hide_header X-Powered-By;

    # Static Next.js bundle — served from disk by nginx, 1y immutable.
    location /_next/static/ {
        alias /var/www/html/my-site.com/public_html/.next/static/;
        expires 1y;
        access_log off;
        add_header Cache-Control "public, max-age=31536000, immutable" always;
        add_header X-Content-Type-Options "nosniff" always;
    }

    # Operator-uploaded images — served from disk, 30d cache. The app
    # also exposes a Node fallback at /uploads/[...path] for hosts
    # that skip this alias (e.g. fronted by a CDN), but disk serving
    # is materially faster on a busy site. /uploads/brochures-private/
    # is gated behind signed download tokens at /api/brochure/[token]
    # — block direct disk access here as defence in depth.
    location /uploads/ {
        alias /var/www/html/my-site.com/public_html/uploads/;
        expires 30d;
        access_log off;
        add_header Cache-Control "public, max-age=2592000" always;
        add_header X-Content-Type-Options "nosniff" always;
        location ^~ /uploads/brochures-private/ {
            return 404;
        }
        # Refuse executable + browser-scriptable extensions (operator
        # media is only raster + gated PDFs).
        location ~* \.(php|phtml|cgi|pl|sh|exe|svg|html?|xhtml|xml|js|mjs)$ {
            return 404;
        }
    }

    location / {
        proxy_pass http://127.0.0.1:8201;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 30s;
        proxy_send_timeout 30s;
    }

    # Block common reconnaissance noise.
    location ~* /(?:\.env|\.git|wp-admin|wp-login\.php|xmlrpc\.php) {
        return 404;
    }
}

In your terminal

Edit the highlighted fields below before you copy. The copy button bakes your values into the snippet, so there is no fumbling at the terminal prompt to fix placeholders after paste.

sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d 

Then open https://<your domain>/install?t=<token> in a browser.

cPanel troubleshooting

Snagged on shared hosting?

Shared cPanel / LiteSpeed runs your app on a private socket inside a memory and connection budget, which causes a handful of recognisable snags. Here is each one, what you see, why it happens, and the exact fix. Most are already handled in a recent version, so the first move is usually to update.

Symptom

Terminal won’t open, or it says shell access is disabled.

SSH is off by default on most shared plans, and the switch lives in a different place per host. Namecheap: cPanel → Manage Shell (their own plugin) → turn SSH on, then Advanced → Terminal works. GoDaddy: the GoDaddy dashboard → Manage → Settings → SSH access. Bluehost and HostGator: cPanel → Security → SSH/Shell Access → Real Shell. A2: already on. If you enabled SSH but there is still no Terminal icon, your host hid the in-browser one, so SSH in from your own computer (ssh user@your-host -p PORT) and run the same commands there.

Nothing works?

One message to your host clears it: “Please enable SSH / shell access for my account.”

Start here

Almost everything is fixed by updating.

The cPanel-specific issues below were resolved across 0.2.2 to 0.2.6. If your install is behind, the cleanest fix for most of this page is a fresh install of the latest release over your existing database, which keeps your content and applies all the fixes at once.

Check your version at the bottom of the admin sidebar (under the CaveCMS mark). The deploy runbook covers the content-preserving reinstall.

The deploy runbook →

In cPanel — Terminal (your app's folder)

# the installer that ships every fix
npx create-cavecms@latest --surface=cpanel

# point at your EXISTING database when asked — your content is preserved

The one move you'll repeat

How to restart the app.

Many fixes end in “restart the app”. On cPanel that means restarting the Node application, not your whole account. Two ways:

cPanel UI

Setup Node.js App → open your app → Restart. This is the canonical way.

Stop, then start

If a restart doesn’t take, use Stop App, wait a minute, then Start App. This also frees any stuck processes from a previous run.

Symptom

The site loads as plain HTML.

No fonts, no colours, no layout, just unstyled text. The CSS and JavaScript aren’t loading.

Why: the bundled server looks for static assets in a specific folder; on a fresh install they hadn’t been placed there yet. Fixed in 0.2.2+

Fix

Update to the latest version (see above), then restart the app. The startup file now stages the assets automatically on every boot. If you just updated, a hard refresh in your browser clears any cached unstyled page.

Symptom

The setup wizard shows 404.

Opening /install on a brand-new install returns “not found”, so you can’t create your admin account.

Why: the app runs on a private socket, so its security layer couldn’t reach its own settings and assumed the site was already set up. Fixed in 0.2.2+

Fix

Update to the latest version. Then open the install link the installer printed at the end. It includes a one-time token (/install?t=…). Open the plain /install without that token and the wizard can’t create the account.

Symptom

Install fails: “max_user_connections”.

The install (or an update) stops at the database step with a max_user_connections error, and re-running doesn’t help.

Why: shared hosts cap how many database connections your account can hold at once, and the host can keep a previous copy of the app running that uses them all up. Fixed in 0.2.3+. The installer now waits its turn and keeps a small connection pool.

If you’re on an older version and hit it now

  1. cPanel → Setup Node.js App → your app → Stop App. Wait a minute.
  2. cPanel → phpMyAdmin, open your database, and on the SQL tab run SHOW PROCESSLIST;. If rows for your database user linger, end each with KILL <Id>;.
  3. Start the app again (or re-run the install), then continue.

Symptom

503 Service Unavailable.

The site (or just the admin) shows “503 Service Unavailable”, sometimes with nothing in the app log.

Why: the web server couldn’t get a response from the app. Three common causes on cPanel: the app is stopped, it ran out of memory (see below), or the account has stuck processes from earlier crashes.

Fix, in order

  1. Confirm the app is started in Setup Node.js App, then restart it.
  2. Check memory (see Out of memory).
  3. If it stays down with a healthy database and enough memory, the account likely has stuck processes. Ask your host to reset (restart) your virtual environment. This clears any wedged processes for your account. It’s a routine, safe request and often the final fix after a rough day of crashes.

Symptom

Your custom login URL 404s.

You changed your login address (in the wizard or Settings → Security) and now both the new and the old URL return 404. You’re locked out.

Why: on a private socket the routing layer read the old login address and the new one never reached it. Fixed in 0.2.4+. The login address now stays in sync automatically.

Fix

Update to the latest version, then change the login address again from Settings → Security. It now applies on the spot, no server file editing.

Symptom

Updates fail early.

The in-dashboard update stops at “Getting ready” or “Preparing your data” and rolls back, leaving you on the old version.

Why: the update’s health check probed a local address the private-socket app can’t answer, and older installs were missing a path the data step needs. Fixed in 0.2.4+

Fix

Because the broken updater is the one trying to update itself, do this one update by reinstalling the latest version over your existing database (deploy runbook). From then on, in-dashboard updates work normally on cPanel. The failed-update window also has a “Show technical details” link with the logs if you want to see exactly which step stopped.

Symptom

Updates fail, and your sitemap and emails point at localhost.

Updates won’t finish, your sitemap and robots.txt come back empty, and the links in your emails point at localhost instead of your domain.

Why: your Site URL is set to a local address like https://localhost:3040. Setup pre-fills this from the address you opened it with, so reaching the wizard over an SSH tunnel or a localhost forward can save localhost by mistake. The updater, sitemap, and emails all read this one setting. Fixed in 0.2.8+. Setup now refuses a localhost Site URL on a live install, and the updater names the address it tried instead of failing quietly.

Fix

Open Settings → General and set Site URL to the real public address visitors use, for example https://yoursite.com (https, no trailing slash). On a recent version, Settings → Updates shows a notice with a one-click link whenever your Site URL is localhost, so you catch it before an update fails.

Symptom

Mystery 503s under load, or out of memory.

The site goes down with no clear error, especially right after a new version is available, or while doing heavier work, then recovers, then drops again.

Why: shared hosting caps your whole account’s memory, and the app didn’t know its ceiling. Fixed in 0.2.5+. Fresh installs now size themselves to your plan’s memory and pull updates in a memory-polite way.

Fix

The memory tuning is applied at install time, so a fresh install of the latest version is what enables it. Settings → Updates now shows your plan’s memory and flags it when it’s tight. If it’s tight, the real fix is a bigger plan. Ask your host to raise your account to 2 GB of memory, which gives a CaveCMS site comfortable headroom.

Still getting repeated 503s (or a 508 “Resource Limit Reached”) even on a roomier plan? On CloudLinux hosts your account has hard per-account caps (called LVE) on memory and on concurrent processes, and the app runs inside a virtualized filesystem (CageFS). Ask your host to raise your account’s physical memory and entry-process (EP) limits, and if files ever seem to go missing or stale, to rebuild your CageFS. One message does it: “Please raise my LVE memory and entry-process limits, and rebuild my CageFS.”

Symptom

A restore sticks on “Verifying”, or reverts.

A restore puts your content and media back, reaches “Verifying”, then either spins forever or undoes itself and shows the old site.

Why: the final health check used the same local address the private-socket app can’t answer, so it timed out and rolled back a restore that had actually succeeded. Fixed in 0.2.6+. Restores now verify over your real address.

Fix

Update to the latest version and restore again. Note: an encrypted backup can’t be restored from the browser uploader. Restore it with the CaveCMS command line on the server that holds its key. And after any restore, the app signs everyone out so you log back in as a restored user. If a page looks stale, clear your browser cache and sign in fresh.

Symptom

After a restore, you’re the “wrong” account.

Right after a restore, your login seems off. A password that should work is rejected, or the admin bar is missing on the public site, or you appear signed in as an old account.

Why: a restore replaces the users table, so your previous browser session points at data that just changed underneath it. Fixed in 0.2.6+. Restores now force a clean re-login.

Fix

Clear your browser cache (or use a fresh private window), reload, and sign in with the restored account’s email and password. To change your own password afterward, use Account → Change password in the admin.

Symptom

Your domain still shows your old site.

You opened your domain and a parking page, a host welcome page, or a leftover from a previous site comes up instead of CaveCMS, even though the app is started and healthy.

Why: the host is serving a stale index.html or a default .htaccess left in the domain’s public_html instead of handing the request to the Passenger app. A static file in the docroot wins over the app, so visitors never reach CaveCMS.

Fix

  1. In cPanel → File Manager, open the domain’s public_html and remove or rename any leftover index.html (or default .htaccess) the host placed there.
  2. In Setup Node.js App, confirm the app’s Application URL points at the right domain, so Passenger serves it from that docroot.
  3. Restart the app and reload your domain. Passenger now rewrites .htaccess on boot, so reload the domain once it’s back up.

The other half

When it’s a host setting, not CaveCMS.

A few things live with your hosting account, not the app. If you hit these, a short, specific request to your host clears them:

Stuck processes

After repeated crashes the account can hold wedged processes that only a virtual-environment restart clears. Ask: “Please reset (restart) my virtual environment to clear stuck processes.”

Memory limit

If the memory readout on Settings → Updates is tight and the site keeps dropping under normal use, ask to raise your plan’s memory to 2 GB.

Database connections

If “max_user_connections” persists with the app stopped and no lingering processes in phpMyAdmin, ask your host to clear stuck MySQL connections or raise the per-user connection limit for your database user.

Node version

CaveCMS needs Node 20.19+, 22.12+, or 23+. In Setup Node.js App, pick a supported Node version for the app; the installer also checks this up front.

Prefer to build by prompt?

Let an AI agent set it up.

Once you’re installed, point Cursor, Claude Code, Windsurf, or Codex at your site and edit it in plain English over the API. Connect your AI assistant →

Built it on your laptop first? An agent can promote your local changes to production with a single push.

Not sure where to host?

Compare your options

VPS, cPanel shared hosting, or your laptop, side by side.

What you need

Almost nothing.

No account, no platform to sign up for. CaveCMS lives entirely on your server. We never see your data.

A small Linux VPS

1 GB RAM minimum, 2 GB recommended. Ubuntu 22.04 LTS tested. Hetzner, DigitalOcean, Linode, Vultr: anything modern.

A domain name

Your own .com (or .studio, or .church, or .anything). Pointed at your VPS via an A record.

Five working terminal commands

mysql, nginx (or apache2), node, pm2, certbot. Most distros ship them in two apt installs.

Supported platforms

It runs on what
you already have.

The installer detects where it's running and configures itself, whether that's a Linux server, your Mac, or shared cPanel hosting. Here's exactly what each platform needs before you run the command above.

Recommended

Linux

Ubuntu, Debian & any systemd distro

The production path. A small VPS with nginx or Apache, x64 or ARM, from the price of a coffee a month. HTTPS lands automatically via Let’s Encrypt.

macOS

Apple Silicon & Intel

Run it on your Mac as a local install: one command, one Node process, no server to manage. Ideal for building client sites, or trying CaveCMS before you ship.

cPanel

Shared hosting, no root

Already on cPanel? The installer wires CaveCMS through Node.js Selector and Passenger, so there is no server to administer and no command line required.

On Windows? CaveCMS runs inside WSL2 + Ubuntu, which behaves exactly like a Linux server. Native Windows isn’t a deployment target.

Node.js
20 or newer
Database
MariaDB 10.11+ (10.6 min)
Architecture
Apple Silicon, Intel & ARM
Tooling
wget · unzip · curl
Memory
1 GB min, 2 GB recommended
Day two

Keeping it current.

Once you are live, updates are one click. New releases appear as a banner in your admin under Settings → Updates. Press Update now and CaveCMS installs it behind a maintenance screen with an automatic safety net: if the new version does not come up healthy, it restores the old one on its own. Encrypted backups run on their own schedule, so a bad day is always recoverable.

Prefer the terminal, or need to undo an update yourself? These run from your install directory, and the cavecms shortcut is set up for you. Full reference on the docs.

Install the latest release

cavecms update

Roll back to the previous version

cavecms rollback

See what's running

cavecms status
Stuck?

We answer every
first-day question.

A real person will help you get from zero to live. No tickets, no scripts, no upsells.