Hosting & deployment

A Minamo site has no database and no required external service. What it needs is Node, the real git binary, and a permanent disk. You deploy a site once; after that you move changes with pull and push.

Requirements

  • Node 22.16 or later, enforced at boot — a site refuses to start on anything older. The search index’s full-text engine (node:sqlite with FTS5) isn’t available before this patch.
  • The git binary on PATH, and the site root must be a real git repository. Content, drafts, and every publish are real git operations; there is no fallback.
  • A permanent disk for the whole site (content/, theme/, media/, vhost/data/), not just media. Publishes are commits made by the running server itself — without a permanent disk, every one since the last deploy is lost on restart.

Choosing a host

Any host works if it gives you an always-on Node server (22.16 or newer), a permanent disk, and git.

WorksDoes not work
Railway, Fly.io, Render (paid plans), NorthflankVercel, Netlify, Cloudflare Pages
Your own server: Hetzner, DigitalOcean, Linode, Vultr, BinaryLane, AWS LightsailHeroku, DigitalOcean App Platform
A server running Coolify, Dokku or CapRoverClassic shared hosting

The hosts that don’t work run code in short bursts with no permanent disk, so editors’ changes would be lost.

Putting a site live for the first time

The first deploy copies everything in your site folder onto the host: the CMS, your theme, your content and your images. Every host needs a permanent disk mounted at /site.

HostFirst deploy
Railwayrailway up --no-gitignore from the site folder
Fly.iofly deploy --dockerfile vhost/Dockerfile from the site folder
Your own serverdocker compose up -d --build on the server
Render, Coolify, and hosts that deploy from GitHubPush the site to GitHub

You only do this once per site. Deploying again later never changes the live site’s content or theme — those belong to the live site now, where editors are changing them. Use pull and push from here on.

How the Docker image works

create-site scaffolds a working Dockerfile and docker-entrypoint.sh in vhost/. The image bakes the site into /seed at build time; on first boot it copies that into the empty /site volume, and runs the server against /site from then on.

Terminal
docker build -f vhost/Dockerfile -t my-site .
docker run -d -p 3000:3000 -v my-site-data:/site my-site

Every later boot refreshes only the installed CMS from the image (vhost/package.json, its lock file and node_modules), so redeploying with a newer CMS upgrades a running site without touching its content. Content, drafts, media, the theme, and vhost/site.config.json (its tokens) belong to the live site and are left alone. Without the volume, the container’s writable layer is thrown away on every redeploy, and every publish with it.

Health-check against GET /v1/capabilities — a real, always-available endpoint that doubles as a liveness check.

Without Docker

On your own machine or server, install Node 22.16+ and git, then:

Terminal
cd vhost
npm install
npm start

Run it under a process supervisor (systemd, pm2) so it restarts on crash or reboot, and put a reverse proxy (nginx, Caddy) in front for TLS — set trustProxy: true once you do.

site.config.json reference

All fields are optional. A missing file, or a missing field, falls back to the default shown — the safe, default-secure state (a zero-token site fails closed on every write route), not an error. A malformed file is a hard startup failure.

FieldDefaultNotes
port3000Must match the scaffolded Dockerfile’s EXPOSEd port if built unmodified. Overridden by the PORT environment variable if set.
tokens[]Array of { hash, scopes }. hash is a sha256 hex digest of the real token, never the raw token. Rotating a token requires a restart.
rateLimit{ max: 60, windowMs: 60000 }Per-IP.
trustProxyfalseSet true when a reverse proxy or platform edge sits in front, so client IPs are read correctly.
ipAllowlist[]Empty means no restriction, not “nothing allowed”.
checkpointIntervalMs1800000 (30 min)How often open drafts are auto-committed as a safety checkpoint.
media.maxUploadBytes10485760 (10MB)
adminBaseUrlunsetAn absolute http(s) URL for whichever admin manages this site. When set, GET /admin redirects there with ?site=<host> appended, so visiting yoursite.com/admin jumps straight to that site in the admin. When unset, /admin isn’t reserved at all.

Backing up media

media/ is deliberately not git-tracked — it’s binary, uploaded content, not source. Back it up with the rest of the volume (a scheduled snapshot, rsync, a tarball). npm run pull also downloads every image the live site has into a local copy.

Local dev tunnel

npm run tunnel exposes a locally running site through a public HTTPS URL, printed on boot — useful for pointing a hosted admin at a site you’re developing, without deploying it. The tunnel closes on shutdown, and a failed tunnel never stops the site itself from starting.

A tunnel is public for as long as it runs

Anyone with the URL and a valid access token can reach it. Don’t leave it running unattended, and don’t use it as a substitute for real hosting.