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
- Makes the repository on GitHub (private, unless you add
--public), from your template if you name one. - 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.
- 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.
- 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 onprocess.env.PORT, atprocess.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.ymlthat runs on pushes tomain.
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:
- ignores anything unsigned, or not a successful run of CI for a push to
mainin the product's own repository; - fetches the repository with the product's own read-only key, and checks GitHub's host key against the ones GitHub publishes;
- deploys only if the commit is still the newest on
main, so an old message sent again can't roll it back; - refuses a release over 1GB or 200,000 files, or one the disk hasn't room for (it always keeps 1GB free);
- 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;
- 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
| Command | Where | Does |
|---|---|---|
check | Server | Checks the server is set up safely, and says what to fix |
install --domain <d> | Server | Installs modest-host's own parts, once |
launch <name> <domain> --server <s> | Laptop | Makes the repository, adds the product, and connects them |
add <name> --domain <d> --repo <o/r> | Server | Adds a product (launch does this for you) |
list | Server | The products, their domains, and what each is running |
deploy <name> | Server | Deploys main's newest commit now, whether or not its CI passed |
restart <name> | Server | Restarts a product, after changing its keys |
logs <name> | Server | A product's latest log lines |
backup | Server | Copies every product's databases now (it runs nightly anyway) |
remove <name> | Server | Takes 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
| What | Where |
|---|---|
| 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 log | journalctl -u modest-host-<name> |
| The deploys' log | journalctl -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.