Skip to content
sitesolide

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
  • 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 / Read and Zone / 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_ed25519 or another.
  • A machine: either a Hetzner Cloud API token, and sitesolide machine orders one, or any fresh Debian 13 VM, from any provider, that you reach as root with that key.
curl -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.

At 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.

read -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.

cd 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.

One 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.

Colleagues 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.

Cloudflare 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, replace cloudflare with your provider's Caddy module and the variable with whatever it wants, add that module on the machine with caddy 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.

Possible 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.

What 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 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

"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.