Docs Install
Installing sitesolide
From nothing to a first deployed project in four commands. Budget a quarter of an hour, most of it the machine installing packages while you wait.
curl -fsSL https://github.com/cthiriet/sitesolide/releases/latest/download/install.sh | sh
sitesolide machine create --provider hetzner --name web
sitesolide setup root@203.0.113.10 --zone example.com --email you@example.com
cd your-project && sitesolide deploy
What you need first
Link to the section What you need first- A domain whose DNS is at Cloudflare. Every project gets a subdomain of it, and the machine serves the bare domain too. Another DNS provider works with a few more steps: see Another DNS provider.
- One Cloudflare token, created at
dash.cloudflare.com/profile/api-tokens
with Create Custom Token, restricted to that zone, with
Zone / Zone / ReadandZone / DNS / Edit. Setup uses it from your workstation to create the records, and lays it on the machine, where Caddy uses it for the wildcard certificate. - An SSH key on your workstation,
~/.ssh/id_ed25519or another. - A machine: either a Hetzner Cloud API token, and
sitesolide machineorders one, or any fresh Debian 13 VM, from any provider, that you reach asrootwith that key.
1. Install the CLI
Link to the section 1. Install the CLIcurl -fsSL https://github.com/cthiriet/sitesolide/releases/latest/download/install.sh | sh
One executable for your system and processor, macOS or Linux, checked against
the release's checksums and put in ~/.local/bin. It needs neither Bun nor a
clone of this repository: everything the CLI runs is inside it. Your
workstation needs only what it already has, bash, ssh, rsync and curl.
From a checkout instead, for development: ln -sf "$PWD/bin/sitesolide.ts"
~/.local/bin/sitesolide, with Bun installed.
2. A machine
Link to the section 2. A machineAt Hetzner, with a token of a project of its own (Security, API tokens, Read & Write):
read -rs HCLOUD_TOKEN && export HCLOUD_TOKEN
sitesolide machine create --provider hetzner --name web
It uploads your public key, creates a firewall that opens 22, 80, 443 and ICMP,
orders a cx23 (2 vCPU, 4 GB) running Debian 13, and waits until SSH answers,
under a minute. --type, --location and --backups change the defaults;
sitesolide machine list and sitesolide machine destroy do the rest. See
machine.md.
Anywhere else, create a Debian 13 VM with your key for root, and go on.
3. Install it
Link to the section 3. Install itread -rs CLOUDFLARE_API_TOKEN && export CLOUDFLARE_API_TOKEN
sitesolide setup root@203.0.113.10 --zone example.com --email you@example.com
--email is the address the certificate authority warns about expiry;
--contact, optional, is shown to a visitor who lands on a locked preview.
Setup hardens the machine (a deploy account with sudo, ssh by key only and
closed to root, ufw, fail2ban, automatic security updates), creates the four
DNS records, installs Caddy with its DNS module and Bun, then deploys every
service of the platform in the order a fresh machine accepts: the shared API,
the dashboard and the root daemon behind it, the portal, the rule that isolates
services on the loopback, the monitor, backups, the installer that deploys
with a token, and the egress proxy. It writes ~/.config/sitesolide/config.json, which every other
command reads.
It shows two passwords, once each: the dashboard's, and the portal's, the owner's password at the sign-in of restricted sites. Store them in a password manager before the terminal scrolls them away; both are changed from the dashboard afterwards.
It can be run again at any time. On a machine already installed it reads everything and changes nothing; after a failure it resumes at the step that failed. Every step, what it checks and what it changes, is in setup.md.
The dashboard is then at https://dashboard.example.com.
4. Deploy something of your own
Link to the section 4. Deploy something of your owncd your-project && sitesolide deploy
A folder without sitesolide.json gets one inferred from what it holds:
sitesolide detect shows it, sitesolide deploy --yes writes it and deploys.
The repository's examples/static-site, examples/bun-app and
examples/background deploy as they are. Each answers at
https://<slug>.example.com.
To put it on its own domain, add it to the manifest, point its DNS at the machine, and:
sitesolide domain --activate
That writes domain.active, puts the manifest on the machine and rebuilds the
domain table, the last step being the one that authorises the certificate.
5. Know when something breaks
Link to the section 5. Know when something breaksOne machine serves everything, so its failures are everyone's. The monitor setup installed checks every minute that Caddy runs, that every site answers over HTTPS, that no service failed, and that disk, memory, certificates and backups are fine. Out of the box it only writes to the journal. Give it a heartbeat, a free healthchecks.io check that alerts when the pings stop, which is the only thing that notices the machine itself dying, and optionally a Slack, Discord or ntfy webhook: five minutes, described in monitor/README.md. What the monitor finds down also shows among the dashboard's Issues.
6. Let others in, without SSH
Link to the section 6. Let others in, without SSHColleagues and agents deploy with a personal token instead of root SSH: create
one on the dashboard's Tokens page, and send its holder to access.md,
"People and tokens". Setup installed what it needs, unless it ran with
--minimal. Once signing in with a company account is set up
(portal/README.md), give
colleagues a role on a project instead, from its Access section in the
dashboard or with sitesolide share <email> --role developer in its folder:
they sign in to the dashboard and mint their own tokens, never stronger than
their roles.
Another DNS provider
Link to the section Another DNS providerCloudflare is named in two places: setup's records, and the Caddyfile's
(tls-zone) snippet, dns cloudflare {env.CLOUDFLARE_API_TOKEN}, which
obtains the wildcard certificate.
- Run setup with
--skip-dns: it prints the four records to create,<zone>and*.<zone>, A and AAAA, and waits for them to resolve. - In
infra/caddy/Caddyfile, replacecloudflarewith your provider's Caddy module and the variable with whatever it wants, add that module on the machine withcaddy add-package, and lay/etc/caddy/cloudflare.env, root:caddy 0640, with its variables before running setup. This needs a checkout: the binary carries the Caddyfile as released.
No wildcard at all
Link to the section No wildcard at allPossible but poorer: each project then gets its own
certificate over HTTP-01, which works, costs an issuance per subdomain, and
makes previews visible in certificate transparency logs. Remove the
import tls-zone lines and let Caddy do its default thing.
Installing by hand
Link to the section Installing by handWhat setup runs, from a checkout, for whoever wants every step in front of
them. The hardening setup does first is described in setup.md;
on another machine, do its equivalent by hand. These run once, in this order,
each one idempotent: the order is the one a fresh machine accepts, every step
leaning on the one before it. sitesolide
init --server deploy@203.0.113.10 --zone example.com --email you@example.com
first, so that the scripts know the machine.
# Caddy from its own repository, then the Cloudflare DNS module the wildcard
# certificate needs, which the standard build lacks
ssh you@203.0.113.10 'curl -1sLf https://dl.cloudsmith.io/public/caddy/stable/gpg.key | sudo gpg --batch --yes --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg && curl -1sLf https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt | sudo tee /etc/apt/sources.list.d/caddy-stable.list && sudo apt-get update && sudo apt-get install -y caddy && sudo caddy add-package github.com/caddy-dns/cloudflare'
# Bun, at /usr/local/bin/bun, where every unit looks for it. Its installer
# needs unzip, which a bare Debian image lacks
ssh you@203.0.113.10 'sudo apt-get install -y unzip && curl -fsSL https://bun.com/install | sudo BUN_INSTALL=/usr/local bash'
# The Cloudflare token Caddy reads for its certificates
ssh you@203.0.113.10 'sudo install -m 0640 -o root -g caddy /dev/stdin /etc/caddy/cloudflare.env' <<< 'CLOUDFLARE_API_TOKEN=your-token'
caddy add-package replaces the package's binary, and an upgrade of the
package puts the standard one back: run it again after every apt upgrade
that touches Caddy, or the next reload refuses the configuration for want of
the DNS module.
Then Caddy's configuration, in three moves:
# Lays the zone file and an empty domain table, then stops: Caddy's unit does
# not load the zone variables yet
bin/deploy-caddy.sh
# The drop-in that loads them, and restarts Caddy whatever the way it stopped
ssh you@203.0.113.10 'sudo mkdir -p /etc/systemd/system/caddy.service.d && sudo install -m 644 /dev/stdin /etc/systemd/system/caddy.service.d/override.conf && sudo systemctl daemon-reload && sudo systemctl restart caddy' < infra/caddy/caddy.service.d/override.conf
# The Caddyfile itself; the first certificates take about a minute
bin/deploy-caddy.sh
That stop is not decorative: the Caddyfile reads $SITESOLIDE_ZONE, and
reloading Caddy without it would serve empty addresses on every site at once.
On a first install the script waits up to two minutes per address for the
authority to issue the certificates; on a machine already serving it does not
wait, since a silent address there is an outage. The drop-in also sets
Restart=always, see
infra/README.md.
Then the two services that need nothing else:
bin/deploy-api.sh # on-demand TLS and preview locks, and its account
bin/deploy-gatekeeper.sh # the only thing that touches Caddy from the machine
The dashboard and the portal
Link to the section The dashboard and the portalThe dashboard first, with its password: it is the one secret the dashboard cannot create for itself, since it is what opens it. The steward and the collector come right after, because both need the dashboard's account and code to exist.
bin/dashboard-password.sh # shows the password once, puts its hash on the machine
cd dashboard && sitesolide deploy
bin/deploy-steward.sh # the root daemon that writes secrets
bin/deploy-collector.sh # the timer that snapshots the machine for the dashboard
The dashboard is then at https://dashboard.your-zone.tld. Every other secret
is created there. The portal's comes next:
cd portal && sitesolide deploy
It stops on portal.env, missing on the machine, and says where to create it:
in the dashboard, Secrets, the portal's portal.env, then Change password,
which shows the portal's password once. Run the deploy again.
Last, the rule that isolates the services from one another, and the monitor. The rule checks that the dashboard reaches the portal, so it comes once both are deployed:
bin/deploy-loopback.sh close # the nftables rule that isolates the services
bin/deploy-monitor.sh # the timer that checks every site each minute, and alerts
Then the optional components, in this order: the steward runs again after each one that lays something it reads.
bin/deploy-backup.sh install && bin/deploy-steward.sh && bin/deploy-backup.sh enable
bin/deploy-installer.sh # deploys with a token
bin/deploy-egress.sh && bin/deploy-steward.sh
Troubleshooting
Link to the section Troubleshooting"Host key verification failed" on a machine you just created. The provider
gave it the address of a machine you used before, and ssh remembers the old
one's key: ssh-keygen -R 203.0.113.10, then run the same command again. On a
machine you did not just create, stop instead: something else answers there.
ssh suddenly answers "connection refused". fail2ban banned your address
after failed logins, for ten minutes by default: wait, or from the provider's
console run fail2ban-client status sshd and fail2ban-client unban <address>.
caddy validate refuses a configuration that looks fine. Run by hand it
does not load /etc/caddy/cloudflare.env or /etc/caddy/sitesolide.env, so the
DNS module sees an empty token and the addresses are empty. That refusal is not
a defect of the file. Use bin/deploy-caddy.sh, which loads both the way systemd
does.
Never caddy stop or caddy start. They talk to the admin API on
127.0.0.1:2019, that is to say to production, whatever --config you pass.
sudo systemctl reload caddy applies a configuration without an interruption.
A project answers 404 under the wildcard. Its directory exists but nothing
serves it: either public/ is empty or the service is not running.
sitesolide status says which.
A preview asks for a code you do not have. The site's Access section in
the dashboard shows it, to the owner and the site's Admins; sitesolide lock
in the project's folder prints the code in force without changing it.