Docs

Using modest-deploy

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

Before you install

modest-deploy doesn't set up the server. You do that once, and modest-deploy check checks it. Step by step goes through it in detail. On a new Ubuntu 26.04 LTS server you need the list below. 24.04 LTS also works, but it can't stop a compromised product from opening other ports (why).

  • 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

$ sudo npm install -g modest-deploy
$ sudo modest-deploy check
$ sudo modest-deploy install --domain deploy.example.org

Or run it from a clone of the repository, which Node runs as it is: sudo git clone https://github.com/thisismodest/modest-deploy.git /opt/modest-deploy, then sudo ln -s /opt/modest-deploy/src/cli.ts /usr/local/bin/modest-deploy. For launch, install it on your laptop too.

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 installs the deploy listener, the deploy service, and the nightly backup. Point the deploy domain's DNS at the server. That's where GitHub sends its messages.

Launch a product

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

$ modest-deploy 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-deploy 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-deploy/env/<name>.env, one KEY=value a line, then modest-deploy 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-deploy deploy <name>.

Commands

CommandWhereDoes
checkServerChecks the server is set up safely, and says what to fix
install --domain <d>ServerInstalls modest-deploy'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 npm install -g modest-deploy@latest
$ sudo modest-deploy install

From a clone, sudo git -C /opt/modest-deploy pull instead of the first line. Running install again rewrites every unit it made, the products' included, so they use the new version, and restarts those whose unit changed.

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-deploy-shop
$ sudo install -o mh-shop -g mh-shop -m 600 /var/backups/modest-deploy/shop/2026-10-25/app.db /var/lib/modest-deploy/shop/app.db
$ sudo rm -f /var/lib/modest-deploy/shop/app.db-wal /var/lib/modest-deploy/shop/app.db-shm
$ sudo systemctl start modest-deploy-shop

Where things live

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

Copy /var/backups/modest-deploy 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.