Installing CairnServer, checking it, starting it, and the handful of behaviours that surprise people the first time.
curl -fsSL https://cairnserver.com/install.sh | sudo bash
It fetches a release bundle, verifies its SHA-256, unpacks it and runs the installer inside. It refuses to run on anything but Linux with systemd, and tells you what it will do before it does it.
Cairn manages Caddy, Postfix, PHP-FPM and podman, so it installs them —
a box that cannot serve a site is not a server. acl comes too,
because cairn-monitor needs it to watch config directories.
BIND is not installed. Most boxes do not serve DNS, and
there is no Cairn worker managing it yet — a running BIND with no control
plane over it is worse than none. --no-services skips the lot
if you package your own.
--dry-run show what would happen, change nothing
--version <v> install a specific release
--no-services do not apt-get Caddy, Postfix, PHP-FPM, podman
--verbose list every step rather than collapsing the clean ones
sudo /usr/local/share/cairn/verify-install.sh
Run this before starting anything. The installer creates what the verifier asserts — they are a matched pair. If verify fails after a successful install, that is a bug in Cairn, not in your box.
It matters because the broker fails in confusing ways when its
preconditions are missing: a wrong directory owner surfaces as
PermissionDenied after authorisation has already
logged allowed.
Order matters. The broker first, then the workers, then the API.
sudo systemctl enable --now cairn-broker
sudo systemctl enable --now cairn-web cairn-mail cairn-php cairn-container cairn-fw
sudo systemctl enable --now cairn-api cairn-monitor
Nothing is started by the installer. Provisioning and starting are separate decisions, and the verifier exists to be run in between.
cairn-api mints an admin token the first time it starts and
writes it to /etc/cairn/bootstrap-token (mode 0600).
sudo cat /etc/cairn/bootstrap-token
Use it once, to create a token scoped for whatever you are actually doing, then revoke it. It is the only credential that can mint others.
Every request carries a bearer token, and every token carries scopes. A missing scope is a 403, which looks like a broken client if you have guessed the name — so here they are exactly.
| Resource | Scopes |
|---|---|
| Sites | sites:read sites:write |
| Apps | apps:read apps:write |
| PHP pools | pools:read pools:write |
| Mail domains | mail_domains:read mail_domains:write mail_domains:delete |
| Firewall | firewall:read firewall:write |
| Tokens | tokens:read tokens:create tokens:revoke |
Note the shapes that differ: mail domains have a
separate delete scope, and tokens use
create and revoke rather than
write.
curl -sX POST http://127.0.0.1:9100/v1/tokens \
-H "Authorization: Bearer $(sudo cat /etc/cairn/bootstrap-token)" \
-H 'Content-Type: application/json' \
-d '{"name":"deploy","scopes":["sites:read","sites:write"]}'
The response contains raw_token. It is shown once.
cairn-api listens on 127.0.0.1:9100 only. It is
not exposed to the network, and should not be — reach it over an SSH tunnel,
or from something running on the box.
| Route | Purpose |
|---|---|
/v1/sites | Caddy sites |
/v1/apps | containers, as podman quadlets |
/v1/pools | PHP-FPM pools |
/v1/mail | Postfix virtual domains |
/v1/firewall | the nftables ruleset |
/v1/tokens | API tokens |
There is no health endpoint. To check a box is reachable and your token works, ask it for one site — a box that answers TCP but rejects your token is not one you can manage.
curl -so /dev/null -w '%{http_code}\n' \
-H "Authorization: Bearer $TOKEN" \
'http://127.0.0.1:9100/v1/sites?limit=1'
curl -sX POST http://127.0.0.1:9100/v1/sites \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name":"blog","hostnames":["example.com"],"kind":"static"}'
kind is proxy, static or
php. A static site serves from /srv/<name>;
the path is derived from the name, never sent by the caller.
Only hostnames can be changed afterwards. A rename would point a live site at a socket nothing publishes, and the kind selects the renderer — Cairn treats both as a different site rather than an edit.
Every other resource can be got wrong and fixed by trying again. A firewall cannot — a rule that severs SSH removes the means of correcting it.
So an apply is provisional. PUT /v1/firewall loads the ruleset
and arms a revert timer; you have two minutes to prove you
can still reach the box by calling confirm. Miss it and the previous ruleset
comes back on its own.
curl -sX POST http://127.0.0.1:9100/v1/firewall/confirm \
-H "Authorization: Bearer $TOKEN"
It means no ruleset has been written yet, which is true of every box that has just been installed. Not an error.
The config was written but the service could not be reloaded — usually because that service is not installed. The database row is not created, deliberately: Cairn does not claim a site it could not bring up, so you are left with a file nothing is running rather than a record of something that does not work.
Every worker must be in the cairn-workers group, which is what
opens /run/cairn/broker.sock. verify-install.sh
checks this.
cairn-monitor is deliberately not in that group. It
audits the broker, and a monitor that can ask the broker about the broker is
asking the suspect to vouch for itself. It reads binaries through
cairn-binaries instead.
cairn-monitor embeds a hash of every binary when it is built. If
the binaries on disk were not built from the same tree, they will not match.
Rebuild all of them together and redeploy.
Known. The broker's path table is compiled for PHP 8.3; Debian 13 ships 8.4. Sites and containers are unaffected.