Docs

Using modest-host

Set a server up once, then launch products with one command, and deploy them on every push.

Before you install

modest-host doesn't set the server up: your hosting provider and a few minutes by hand do that better, and modest-host check checks the result. On a new Ubuntu server:

  • Sign in with an SSH key, added when you make the server, as your own user with sudo. Turn off signing in by password, and as root.
  • A firewall that lets in only SSH (from your own address, if you can), HTTP, and HTTPS. Most providers have one in their control panel.
  • Automatic security updates: apt install unattended-upgrades.
  • Node 24 or later, and Caddy, from their own package repositories.

Install, once per server

$ sudo git clone https://github.com/thisismodest/modest-host.git /opt/modest-host
$ sudo ln -s /opt/modest-host/src/cli.ts /usr/local/bin/modest-host
$ sudo modest-host check
$ sudo modest-host install --domain deploy.example.org

Node runs it from the repository as it is, with nothing to build or install. For launch, clone it on your laptop too, and link it the same way.

check lists what it relies on, each marked ok or with what to fix. install runs it first, and won't go on until everything passes (add --anyway if you're sure). Then it adds its own parts: the deploy listener, the deploy service, and the nightly copies. Point the deploy domain's DNS at the server: GitHub's messages arrive there.

Launch a product

From your laptop, with gh signed in and SSH access to the server:

$ modest-host launch shop shop.example.org --server myserver --template example/template
  1. Makes the repository on GitHub (private, unless you add --public), from your template if you name one.
  2. Adds the product on the server: its own user, folder, keys file, service, and its domain in Caddy, which gets and renews its HTTPS certificate.
  3. Connects them: gives GitHub the server's read-only deploy key, and a webhook for the "Workflow runs" event, signed with a secret made for this product.
  4. Tells you what's left: point the domain's DNS at the server, and push.

On the server, modest-host add does the second step alone, and prints what to add on GitHub by hand.

What a product does

  • Starts with node server.js, and listens on process.env.PORT, at process.env.HOST (127.0.0.1). A version listening anywhere else isn't deployed, so nothing reaches it but through Caddy.
  • Keeps what it writes in process.env.DATA_DIR. Everything else is read-only to it.
  • Answers GET / once it's ready, with anything but a server error.
  • Has a lockfile (package-lock.json) if it has dependencies.
  • Has CI at .github/workflows/ci.yml that runs on pushes to main.

Its keys, such as an email service's, go in /etc/modest-host/env/<name>.env, one KEY=value a line, then modest-host restart <name>. With modest-auth, name Caddy as the proxy in front: trustedProxies: ["loopback"].

Every push

When CI passes on main, GitHub sends a signed message to the deploy domain. The server:

  1. ignores anything unsigned, or not a successful run of CI for a push to main in the product's own repository;
  2. fetches the repository with the product's own read-only key, and checks GitHub's host key against the ones GitHub publishes;
  3. deploys only if the commit is still the newest on main, so an old message sent again can't roll it back;
  4. refuses a release over 1GB or 200,000 files, or one the disk hasn't room for (it always keeps 1GB free);
  5. builds the release as a separate user, with dependencies exactly as the lockfile says and install scripts off, then makes it owned by root and readable only by the product;
  6. switches to it, restarts the product, and checks it answers within a minute, and only on 127.0.0.1; if not, the version before comes back.

The last three releases are kept. To deploy again by hand: sudo modest-host deploy <name>.

Commands

CommandWhereDoes
checkServerChecks the server is set up safely, and says what to fix
install --domain <d>ServerInstalls modest-host's own parts, once
launch <name> <domain> --server <s>LaptopMakes the repository, adds the product, and connects them
add <name> --domain <d> --repo <o/r>ServerAdds a product (launch does this for you)
listServerThe products, their domains, and what each is running
deploy <name>ServerDeploys main's newest commit now, whether or not its CI passed
restart <name>ServerRestarts a product, after changing its keys
logs <name>ServerA product's latest log lines
backupServerCopies every product's databases now (it runs nightly anyway)
remove <name>ServerTakes a product off; its data, copies, and keys file stay

On the server, run them with sudo. Add --anyway to install or add to go on when check finds something to fix.

Updating

$ sudo git -C /opt/modest-host pull
$ sudo modest-host install

Running install again rewrites every unit it made, the products' included, so they use the new version, and restarts them.

Restoring a backup

Stop the product, put the copy back as the product's own, remove SQLite's -wal and -shm files (they belong to the database being replaced), and start it:

$ sudo systemctl stop modest-host-shop
$ sudo install -o mh-shop -g mh-shop -m 600 /var/backups/modest-host/shop/2026-10-25/app.db /var/lib/modest-host/shop/app.db
$ sudo rm -f /var/lib/modest-host/shop/app.db-wal /var/lib/modest-host/shop/app.db-shm
$ sudo systemctl start modest-host-shop

Where things live

WhatWhere
Each product's settings/etc/modest-host/products/
Each product's keys (its environment)/etc/modest-host/env/
Its code: the releases, and the one running/srv/modest-host/<name>/
Its data/var/lib/modest-host/<name>/
Nightly copies of its databases (7 nights)/var/backups/modest-host/<name>/
Its logjournalctl -u modest-host-<name>
The deploys' logjournalctl -u modest-host-deploy

Copy /var/backups/modest-host off the server too, or turn on your provider's backups.

What it doesn't do

  • Set the server up, as above.
  • Run products that need more than Node, such as a browser. Those are better in a container of their own.
  • DNS, email, or payments. Those stay with your providers.
  • Run across several servers. A product that outgrows a shared one moves to a server of its own, set up the same way.