Install ModernCI
Install and run ModernCI on your own Linux machine: the binary and its signature, setup, backends, backups, webhooks, the cloud link, updates, licence and per-team isolation.
For the operator: the person who runs ModernCI on their own machine for one team or many. This page goes from an empty Linux machine to a running install that checks and merges pull requests.
Each step is marked when it is not on main yet:
- live: in the released binary today;
- PR #n: built in an open pull request, coming in a release soon;
- planned (#n): decided, not built.
1. What you need
A machine. A Linux machine or VM, aarch64 (arm64) or x86_64 (amd64). Ubuntu 24.04 LTS is what we run and test on.
- Releases today:
aarch64only. Anx86_64build is in PR #1009 (#1008). On anx86_64machine, wait for it. - macOS can run the daemon, but releases are Linux only.
- Size it by your checks, which run on this machine when you use its local backend. Our own install runs on 11 CPUs, 24 GiB of memory and a 120 GiB disk.
- If every check runs elsewhere (Latchkey or EC2), 2 CPUs and 4 GiB are enough for the daemon. We haven't measured a minimum.
A user account for the daemon. Create an ordinary user, for example modernci.
- The daemon runs as that user. It never runs as root and needs no root rights of its own.
- Lingering must be on, so the service starts at boot and keeps running after you log out. As root:
loginctl enable-linger modernci.modernci installreminds you if it's off. - Root is needed only for the per-team check users (section 5) and their one-line sudo rules.
Tools on the machine: curl, openssl 3 and xxd to check the download's signature, and git. Ubuntu 24.04 has all four.
Ports in. One port for the web UI, its API and MCP, all over HTTPS.
- Before setup, the install listens on
0.0.0.0:7171, so you can open the claim page from your browser. - After setup, it serves on the port you choose: 443 by default, 8080 on our own install.
- On Linux, a port below 1024 needs a privilege. The daemon tells you how to grant it, or you can pick a higher port.
- Inbound webhooks need no open port: they arrive over the cloud link (section 4.4).
Outbound access (HTTPS, port 443):
| To | Why | When |
|---|---|---|
downloads.modernci.dev | releases and updates | install and every update |
api.github.com, github.com | your repos: pull requests, statuses, merges, webhooks, clones | always |
modernci.dev (wss://modernci.dev/cloud/link/<tenant>) | the cloud link: webhooks relayed in, the cloud console | only if you link |
api.latchkey.dev and Latchkey's job storage | the Latchkey backend | only if you use it |
| AWS EC2 endpoints in the regions you allow, plus SSH to the machines it starts | the EC2 backend | only if you use it |
2. Get the binary
Releases are signed with ModernCI's Ed25519 release key. Its public half is built into every binary (crates/daemon/release.pub), and is:
395a444767159e296454e894061e1f158c49ec9bd9e6101a10676119a742535b
Releases are published to https://downloads.modernci.dev, a public bucket. live: it holds 0.4.1644 for aarch64-unknown-linux-gnu. Automatic publishing on every release, the x86_64 build and the install script that reads this bucket are in PR #1009 (#1008).
latest.json: the newest release's version, commit and, for each target, file, size, sha256 and signature.<version>/<target>and<version>/<target>.sig: the binary and its signature (hex). These never change once published.<version>/manifest.json: the same facts aslatest.json, for that version.
Once PR #1009 is released: one line
curl -fsSL https://modernci.dev/install.sh | sh
The script finds the newest release for your machine and checks its signature with OpenSSL 3 against the pinned key. Only then does it run modernci install. It refuses an OS or architecture that has no release and names the ones that do.
Not yet: the copy of install.sh on modernci.dev today is not the script. The website redeploy is pending, and the script on dev still reads GitHub Releases of a private repo (#939). Until then, do it by hand.
By hand (works today)
Run this as the daemon's user:
KEY=395a444767159e296454e894061e1f158c49ec9bd9e6101a10676119a742535b
T=aarch64-unknown-linux-gnu # x86_64-unknown-linux-gnu once PR #1009 is released
V=$(curl -fsS https://downloads.modernci.dev/latest.json | sed -n 's/.*"version": *"\([^"]*\)".*/\1/p')
mkdir -m 0700 -p ~/modernci-$V && cd ~/modernci-$V
curl -fsS -o modernci https://downloads.modernci.dev/$V/$T
curl -fsS -o modernci.sig https://downloads.modernci.dev/$V/$T.sig
# Check the signature before you run anything. It must print "Signature Verified Successfully".
printf '302a300506032b6570032100%s' "$KEY" | xxd -r -p > key.der
tr -d '\n ' < modernci.sig | xxd -r -p > sig.bin
openssl pkeyutl -verify -pubin -inkey key.der -keyform DER -rawin -in modernci -sigfile sig.bin
chmod +x modernci
./modernci install --require-signature --release-key "$KEY"
modernci install checks the signature again (modernci.sig sits beside the binary) and refuses to install an unsigned or wrongly signed binary.
What modernci install does live
- Copies the release to
~/.modernci/bin/current/modernci.MODERNCI_HOMEmoves the home. - Writes the systemd user unit
~/.config/systemd/user/modernci.service, enables it and starts it. It runsmodernci supervise, which runs the daemon, restarts it if it dies and applies updates. Logs:journalctl --user -u modernci. - Waits for the daemon to answer, then prints: - the setup address; - a one-time claim code; - the SHA-256 fingerprint of the install's own certificate authority.
- It is safe to run again.
modernci service statusshows what is running.
3. First run: setup
Open the address install printed (https://<machine>:7171/setup). Your browser warns about the certificate, because the install signs its own. Check that the fingerprint matches the printed one, then continue. Enter the claim code: it makes you the install's operator and gives you its first host token.
Setup is one list of steps, each recorded once in ~/.modernci/install.json. You can do the same on the machine's console with modernci setup --here, and change any step later through its own operation.
- Address (required). How people reach the UI (
ui_setup): -reach:lan(every interface, the default),localhost, orlink(only through the cloud link). -port: 443 by default. -cert: -iporhostname: a certificate from the install's own CA, trusted once per browser. The CA is at/ca.crt. -files: your own certificate and key.
In a VM behind NAT, name the host's LAN address yourself, for example modernci call ui_setup '{"host":"10.1.7.14"}'.
- Tenants (required). A tenant is one team or company: its repos, people, secrets and results, kept apart from every other tenant's. Add more later with
modernci tenant <name>. - Compute: where checks run (section 4.2).
- Forge: each tenant's GitHub token. Read from stdin, never from the command line:
modernci forge-token --tenant <t> < token-file. - With it, ModernCI reads pull requests, posts statuses, merges and registers webhooks. - A classic token with thereposcope covers all of that. - The minimum for a fine-grained token isn't documented yet (unverified). - Repos:
repo_addfor each repo (section 4.1). - Webhooks: how GitHub tells ModernCI about a push (section 4.4).
- Updates: the release channel (section 4.7).
- Cloud: the optional link to modernci.dev (section 4.6).
- Your agent: mints your coding agent's token once and prints the two lines that connect it: -
modernci init <url> <fingerprint> < agent.token; -claude mcp add --scope user modernci -- modernci mcp.
A step you skip can be done later. Settings that are not set up, or are off by your choice, show on the setup checklist (section 7).
4. Running it
4.1 Repos live
modernci call repo_add --tenant <t> --repo owner/name
Use --forge gitea --root https://… for a Gitea server. ModernCI reads each branch's checks from .ci/<branch>.yaml in the repo. modernci import github .github/workflows/*.yml --to main -o .ci/main.yaml converts GitHub Actions workflows and reports every line it couldn't carry over.
4.2 Backends: where checks run
A backend is a place checks run. The install's own backends serve every tenant as "ModernCI compute". A tenant can also bring its own (set_own_backends).
- This machine (
local, live). Checks run here, each tenant's as its own user (section 5). - Latchkey live. A fresh runner for every job. - Today: the
latchkeyCLI on the host, with a key file. - PR #976: Latchkey's API called directly, with an API key and no CLI or Node. The key needs only thejobs:runscope. - EC2 live. Spot or on-demand machines in the regions you allow. Each one shuts itself down when it has been idle and when the daemon stops renewing it. - Today: the
awsCLI v2 on the host, with a named profile. - PR #981 (#941): the AWS API called directly, with an access key and no CLI. - The IAM user needs the policy in the EC2 backend's setup shows (in PR #981; the policy is built into the binary). It may launch only instances ModernCI tags, touch only what ModernCI tagged, and pass only a ModernCI machine role. - SSH hosts you join (
modernci host join). PR #864.
How you set them up:
- Today live: in the setup wizard's Compute step (this machine, Latchkey, SSH), or by writing the whole backends file:
modernci call set_host_backendswith the YAML astext. EC2 is YAML only (akind: ec2backend in the same file). - PR #974: the Backends page in the UI, signed in as the operator, or
modernci backend add|edit|remove|test|list. - It takes API keys only. The key is sealed in the install's secret store, never in the file, never on the command line. - Each backend shows connected, key invalid, unreachable, or not tested.
The standard machine image for EC2 (what an EC2 check machine starts from) is waiting on an owner decision. Until then, EC2 starts from Amazon Linux 2023, and a short setup: script in the backend's config installs git, a C compiler and Docker. Your repo's own steps install its toolchain: ModernCI installs no language toolchain anywhere.
4.3 Backups (live: a local or mounted folder)
modernci init --backup /srv/modernci-backup # or: modernci call backup_set url=/srv/modernci-backup
- Every hour (
every, in seconds), the daemon writes an encrypted, incremental snapshot of each tenant and of the install's own settings. - The first
backup_setmakes~/.modernci/backup.key. Copy it off the machine: no backup restores without it. modernci call backup_nowtakes a snapshot now.- To restore onto a fresh machine, stop the daemon first:
modernci restore --from <folder> --key <backup.key>. Every secret is resealed under the new machine's own key. s3://andr2://targets are planned. Todaybackup_setrefuses them.- Each snapshot is sealed per tenant: no snapshot holds two tenants, and a tenant's secrets stay sealed under a key of their own. The machine's
master.key,backup.key, job clones and the build cache are never backed up.
4.4 Webhooks
GitHub must tell ModernCI about pushes. Without webhooks, ModernCI polls, which is slower and uses your API budget.
- Through the relay (
webhook_sync via=relay). On a linked install, GitHub sends tohttps://modernci.dev/cloud/hook/<tenant>, and the relay carries each delivery over your install's outbound link. The relay keeps nothing and cannot read the signature; your install checks it. - Live: you turn it on withvia=relay. - PR #1002: on by default for every linked install.via=noneturns it off. - Direct (
webhook_sync url=https://ci.example.com), if your install has a public HTTPS address. - Polling only if neither is set. The repo's health says so.
4.5 People and agents live
modernci token --tenant <t> --who <name> --scope <scope>mints a token, printed once.- People can also sign in to the UI with a local account and password.
- Agents connect over MCP (
modernci mcp) or callmodernci donewhen a branch is ready.
4.6 The cloud link and app.modernci.dev
Optional. Off until you turn it on.
modernci cloud connect --tenant <t>
- This mints a link token, kept in the install's secret store, and prints its SHA-256 hash.
- The daemon then holds one outbound WebSocket to
wss://modernci.dev/cloud/link/<t>. Nothing inbound is opened. - Today the hash must be registered by ModernCI (
modernci cloud link <t> <hash>, run by whoever runs modernci.dev). Send it to us. Self-service linking is planned. - What a link carries: - webhooks (section 4.4); - pages of your install for the cloud console at app.modernci.dev.
- PR #1006: the install pushes nothing up. Older releases push usage records and state summaries; the
cloudoperation lists exactly what goes up and what never leaves. - Binding your cloud sign-in to an account on your install live: 1.
cloud_claimgives a single-use link, valid 15 minutes. 2. Open it while signed in at app.modernci.dev. 3.cloud_bindslists bindings;cloud_unbindundoes one. - Reading and acting from the cloud: - Live: reading. - PR #994: forms. Your install checks the sign-in itself, and each tenant has a ceiling,
read(the default) oroperate. Admin rights, secrets and members never cross. - Signing in to app.modernci.dev is not open to customers yet. Today it admits ModernCI's own operators only. Customer sign-in (accounts, GitHub sign-in) is planned.
4.7 Updates live
The install updates itself.
- Setup records a channel (
update_channel): every new release, or a pinned version. - The daemon looks every 30 seconds and checks each release's signature against the key built into the binary. Nothing on the host can add a key.
- The supervisor checks the signature again, then swaps without stopping work: - it pauses new starts; - it swaps the binary; - the new daemon adopts every running job.
- If the new version isn't healthy within 5 minutes, it rolls back to
bin/prev.
update.jsonand/healthzshow the last swap.- Today the channel reads GitHub Releases of
MattJackson/ModernCIwith the tenant's GitHub token. That repo is private, so a customer can't use this channel yet. - PR #1009:
url: https://downloads.modernci.devinchannel.yamlneeds no token and no tenant.
4.8 Licence (PR #1030)
With no licence key, the install runs on the Free plan.
modernci licence statusshows the plan.modernci licence apply <key>applies a key; it also reads the key from stdin. Run either on the install's host, or use/setup/licencein the UI.- Keys are checked offline against keys built into the binary.
5. Isolation for the local backend: one user per tenant
Checks are code from pull requests. On the local backend, each tenant's checks run as that tenant's own unprivileged user, through one exact sudo rule. They can't read the daemon's keys, store or secrets, or another tenant's work.
- The daemon tests this at startup and before each job.
- If the test fails, no job runs there: the check waits with the reason, and the alarm
backend.isolationfires.
Today: one root step per tenant. Run as root, with DAEMON the daemon's user, W the backend's workdir and TENANT the tenant:
DAEMON=modernci
W=/var/lib/modernci/work
TENANT=acme
CHECK=modernci-check-$TENANT
install -d -o root -g root -m 0755 "$W"
useradd --system --user-group --create-home --home-dir /var/lib/$CHECK \
--shell /usr/sbin/nologin "$CHECK"
chmod 0700 /var/lib/$CHECK
install -d -o "$CHECK" -g "$CHECK" -m 0700 "$W/$TENANT"
printf '%s ALL=(%s) NOPASSWD: /usr/bin/bash --noprofile --norc -s\n' "$DAEMON" "$CHECK" \
> /etc/sudoers.d/modernci-$CHECK
chmod 0440 /etc/sudoers.d/modernci-$CHECK
visudo -cf /etc/sudoers.d/modernci-$CHECK
The install's local backend names every tenant's user at once:
backends:
local:
kind: local
workdir: /var/lib/modernci/work
run_as: modernci-check-{tenant}
- The work folder must be
root:root 0755, with each tenant's folder below it owned by that tenant's user at 0700. Never use a shared group-writable folder: one tenant could plant a config file that another tenant's build reads. - Do not install a toolchain for the check users. The repo's own steps install what it needs.
PR #957 (#940):
workdir: /var/lib/modernci-check-{tenant}/work, made by the daemon on the tenant's first job.- The root step shrinks to creating the user and its sudo rule.
Making even that step unnecessary is tracked in #940.
The proof, run as the daemon's user, prints the check user's uid, then denied:
echo 'id -u; cat ~modernci/.modernci/master.key >/dev/null 2>&1 || echo denied' \
| sudo -n -u modernci-check-acme -- /usr/bin/bash --noprofile --norc -s
6. Day to day
modernci service status: installed, running, version.https://<ui>/healthz: version, last swap, alarms, the UI's certificate.journalctl --user -u modernci: the supervisor's and daemon's logs.modernci call: lists every operation.modernci call <op> --helpshows one operation's arguments.
7. When something is wrong: the setup checklist
PR #995, PR #1004, PR #1010 (#989): the setup checklist page. The operator lands on it after signing in, at /operator/setup. Each tenant's project view has one too.
- Each row covers one setting: - how the UI is reached; - tenants; - each backend's key, last test and breaker; - each tenant's cloud link; - backups; - updates; - webhooks.
- Each row says OK, off, off by your choice, or red (not set up or broken). It also says why, and has a Fix button that runs the operation.
- While anything is red, a banner shows on every page.
- Something you turned off on purpose shows what being off costs, with an Enable button.
Until it is released:
| You see | Look at |
|---|---|
A check waits with backend.isolation | Section 5: the check user, its sudo rule, folder owners and modes. |
PRs are picked up slowly; the repo's health says polling | Webhooks (section 4.4). The alarm hook.silent names the URL and what GitHub got back. |
| The browser can't reach the UI from another machine | reach must be lan. In a VM, forward the port and set host to the LAN address. Check the host's firewall. |
| The certificate warning comes back | The CA changed (a new install). A restore puts back the old CA. |
| No update arrives | channel.json (last look, refusals) and the alarms release.refused and channel.error. |
A backend shows key invalid | Replace its key (PR #974's Backends page, or the file and key today). |