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
- 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-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 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-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:
- 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-deploy 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-deploy'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 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
| What | Where |
|---|---|
| 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 log | journalctl -u modest-deploy-<name> |
| The deploys' log | journalctl -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.