Step by step

From a new server to a live product

Set up an Ubuntu server, install modest-deploy, and launch a first product on your own domain.

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:

  1. Add a DNS record pointing shop.example.org at the server.
  2. 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

ToOn the server
See every product, and what it's runningsudo modest-deploy list
Read a product's logsudo modest-deploy logs shop
Deploy main's newest commit againsudo modest-deploy deploy shop
Back up every database nowsudo modest-deploy backup
Update modest-deploysudo npm install -g modest-deploy@latest, then sudo modest-deploy install
Take a product off the serversudo modest-deploy remove shop (its data stays)

The docs cover each command, where everything lives, and restoring a backup.