What you need
- A server running Ubuntu 26.04 LTS, with at least 1GB of memory. A DigitalOcean droplet or similar is fine. 24.04 LTS also works, but it can't stop a compromised product from opening other ports (why).
- A domain you can add DNS records to: one name for each product, and one for deploys, such as
deploy.example.org. - A GitHub account, and on your laptop, Node 24 or later and GitHub's command line tool,
gh, signed in (gh auth login).
1. Set up the server
You do this once. When you create the server, add your SSH key. Then sign in as root and make a user of your own (here, ann), with your key:
$ adduser ann
$ usermod -aG sudo ann
$ rsync --archive --chown=ann:ann ~/.ssh /home/ann
Sign in again as that user (ssh ann@your-server) before going on. Then turn off signing in by password, and as root:
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
$ sudo systemctl reload ssh
Add a firewall that lets in only SSH, HTTP, and HTTPS. Your provider's control panel usually has one; on DigitalOcean it's under Networking, then Firewalls. Then install Node, Caddy, and automatic security updates:
$ curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
$ sudo apt install -y nodejs caddy unattended-upgrades
The first line adds NodeSource's package repository for Node 24. Caddy comes from Ubuntu's own packages.
2. Install modest-deploy
$ sudo npm install -g modest-deploy
$ sudo modest-deploy check
check lists what it needs, each marked ok or with what to change. When
everything is ok, install it, with the name GitHub will send deploys to:
$ sudo modest-deploy install --domain deploy.example.org
Then add a DNS record pointing deploy.example.org at the server's address. Caddy gets its HTTPS certificate once the record works.
3. Set up your laptop
$ npm install -g modest-deploy
Give the server a short name in your SSH settings, so ssh myserver signs you in:
Host myserver
HostName 203.0.113.7
User ann
Use your server's address and your user. launch runs modest-deploy add on the
server with sudo, over SSH, where it can't ask for your password. On the server, let your
user run that one command without it:
$ echo "ann ALL=(root) NOPASSWD: $(which modest-deploy) add *" | sudo tee /etc/sudoers.d/modest-deploy
$ sudo chmod 440 /etc/sudoers.d/modest-deploy && sudo visudo -c
Everything else still asks for your password.
4. Write a product
A product is a Node app with a server.js that listens on the port and address it's given. In a new folder:
import { createServer } from "node:http";
// modest-deploy gives each product its port, and 127.0.0.1 to listen on (Caddy is in front).
const port = Number(process.env.PORT ?? 3000);
const host = process.env.HOST ?? "127.0.0.1";
createServer((req, res) => {
res.writeHead(200, { "content-type": "text/plain; charset=utf-8" });
res.end("Hello from shop\n");
}).listen(port, host, () => console.log(`Listening on http://${host}:${port}`));
{
"name": "shop",
"private": true,
"type": "module",
"scripts": {
"start": "node server.js"
}
}
And a CI workflow. modest-deploy deploys when this passes on main, so put your tests here:
name: CI
on:
push:
branches: [main]
permissions:
contents: read
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- run: node --check server.js
Keep anything the product writes in process.env.DATA_DIR: everything else is read-only
to it. If it has dependencies, commit a package-lock.json.
5. Launch it
$ modest-deploy launch shop shop.example.org --server myserver
This creates the shop repository on GitHub (private, unless you add --public), sets the product up on the server, and connects the two. Then:
- Add a DNS record pointing
shop.example.orgat the server. - Push the folder to the new repository:
$ git init -b main && git add -A && git commit -m "First version"
$ git remote add origin git@github.com:YOUR-USER/shop.git
$ git push -u origin main
When CI passes, the server deploys it. Open https://shop.example.org, or check on the server:
$ ssh myserver sudo modest-deploy list
$ ssh myserver sudo modest-deploy logs shop
From now on, each push to main that passes CI deploys the new version.
6. Keys and sign-in
A product's own keys, such as an email service's, go in its environment file on the server, one KEY=value a line:
$ sudo nano /etc/modest-deploy/env/shop.env
$ sudo modest-deploy restart shop
For sign-in, modest-auth works with it as it is: keep
its database in DATA_DIR, set origin to the product's address, and name Caddy as
the proxy in front with trustedProxies: ["loopback"].
Day to day
| To | On the server |
|---|---|
| See every product, and what it's running | sudo modest-deploy list |
| Read a product's log | sudo modest-deploy logs shop |
| Deploy main's newest commit again | sudo modest-deploy deploy shop |
| Back up every database now | sudo modest-deploy backup |
| Update modest-deploy | sudo npm install -g modest-deploy@latest, then sudo modest-deploy install |
| Take a product off the server | sudo modest-deploy remove shop (its data stays) |
The docs cover each command, where everything lives, and restoring a backup.