# Commands Every command runs from a project's folder, the one holding its `sitesolide.json`, except `setup`, `upgrade`, `refresh`, `init`, `machine`, `people`, `help` and `--version`. This is what `sitesolide --help` prints. ```text sitesolide setup install a fresh Debian 13 machine, resumable, a no-op once done --zone --email --contact shown on the page that asks for a preview code --user the account that deploys, deploy by default as root --skip-dns create the DNS records by hand: setup lists them and waits --dns-replace replace records that point elsewhere, on your decision alone --cloudflare-token-stdin read the Cloudflare token from standard input --minimal leave out backups, the installer and the egress proxy --any-os go on with a system other than Debian 13, at your own risk --config-dir another installation's own configuration folder --dry-run check every step, change nothing sitesolide upgrade bring every installed component to this release's code, resumable --dry-run list each component, up to date, out of date or missing, change nothing sitesolide refresh its units generated again from the manifest on the server, no code sent --all every project, one at a time, stopping at the first that fails --dry-run show the units it would write, change nothing sitesolide help this list, with or without a configuration; --help after any command sitesolide --version the release this binary was built from, dev from a checkout sitesolide init write ~/.config/sitesolide/config.json --server --zone --email --contact shown on the page that asks for a preview code sitesolide detect the sitesolide.json this folder implies, written nowhere --write write it, never over an existing one --slug name the project, rather than after its folder sitesolide deploy prepare, build, push, install, restart, verify --dry-run show the unit and the fragment, install nothing, build nothing --build with --dry-run: run the build too, the folder's own code, here --compare with --dry-run: build, then list what would change on the server --force switch a hand-written unit to the generated one --yes [--slug ] no sitesolide.json: write the inferred one, then deploy sitesolide status what the server actually runs sitesolide logs [--follow] journalctl for this project --lines how many lines back, 50 by default sitesolide backups this project's data snapshots, read only sitesolide share this project's general access and people with access ... give them access, Can open by default --role can-open, viewer, developer or admin --expires <24h|7d|30d|never> for password access, 7d by default --remove ... take their access away; password access by the name it is listed under sitesolide people everyone with access, their roles, who may create projects --may-create let them create projects, Admin of what they create --no-create take that right back --migrate-without-portal carry access over without the portal's database, when it does not read sitesolide lock [--dry-run] Anyone with the code: the site opens with a code, said once; or the one in force --status wanted / installed / measured, without touching --new-code a new code, the old one no longer opens it sitesolide unlock [--dry-run] back to Public, its code dropped sitesolide domain where this project's own domain stands --activate [--force] switch the site onto it, then rebuild the table --deactivate back to the preview subdomain sitesolide remove --confirm take the project off the machine, for good, its name freed from the token that created it --dry-run show every step, remove nothing sitesolide run -- load the secret from the vault and run sitesolide mcp serve these commands to an agent, over MCP on stdio sitesolide login --url a person with a token: deploy, set secrets and restart without SSH --token-stdin read the token from standard input sitesolide machine create|list|destroy --provider hetzner a VM ordered by API, before setup: sitesolide machine lists the options any command --api go through the dashboard's API even with a server SITESOLIDE_CONFIG_DIR= before any command: read that installation's configuration --json, on every command but init and run: one JSON event per line, see docs/agents.md secrets live on the server: manage them in the Secrets section of https://dashboard. general access of a deployed site is set from the dashboard too: deploy follows the server ``` `sitesolide setup` is the whole base install of [install.md](install.md), hardening and DNS records included, in one command that can be run again at any time: see [setup.md](setup.md). `sitesolide upgrade` is its other half, for a machine in service: it brings the components setup installed to the code of the binary or the checkout it runs from, and redeploys only those that differ, see [Upgrading the machine](#upgrading-the-machine). `sitesolide refresh` gives the apps already deployed the units this release generates, without sending their code, see [Refreshing the apps' units](#refreshing-the-apps-units). `SITESOLIDE_CONFIG_DIR` points every command at another configuration folder than `~/.config/sitesolide`, for a second installation driven from the same workstation. `sitesolide run` loads the files the manifest declares from `~/.config/sitesolide/secrets/`, the few credentials your workstation presents to production itself, then runs the command. See [secrets.md](secrets.md). `sitesolide deploy` reads the machine before anything is pushed, and refuses there what a cloned folder could reach through it: a secret file that is not the project's, `.env` and `-` being its own, never the machine's, never one a more specific site deployed there holds, and when present a plain file of `site-`, whose owner it reads through `sudo` without opening it; a `publicDir` that is a link out of the repository holding the manifest, unless that manifest is in your sites repository; and a link among the public files that leads out of them, which Caddy would follow on the machine. Each refusal names the file or the link, and nothing was sent. See [manifest.md](manifest.md#secrets). `sitesolide remove` deletes the secret files the folder's manifest declares only when they are the project's by that same rule, and prints the others, `kept:` and why: the manifest is whatever the folder holds now, and a clone's naming `dashboard.env` had root delete the dashboard's password. It reads the sites on the machine and the files' owners first, and an answer that does not read deletes nothing. Its name is free again for a token that may create projects, once the machine keeps neither the removed project's backup snapshots nor its connector grants: until then a token creating it is refused `reserved`, and you deploy it over SSH, or clear what was left ([dashboard/src/backup/README.md](../dashboard/src/backup/README.md#a-name-a-removed-project-left-behind)). `sitesolide logs` shows every service of a project, interleaved by time, the runs of its scheduled services among them, each under its own unit, `..service`. A run that failed says why there, and a deployment that failed because a service fell right after its restart points there too. It reads the journal through `sudo -n`: the services write to the system journal, which the deployment account `setup` makes cannot read on its own, and that account already holds sudo without a password. `sitesolide status` reads what the machine runs, one line per unit under each project. A worker reads as any service, `active` with its memory. A scheduled service shows its schedule, when it last ran and how that run ended, and when it runs next. Between two runs it is `inactive`, its normal state and no outage, or `failed` after a run that failed, until the next one. The main unit of a project whose services are all scheduled, its anchor, is `active` and holds no memory, since it runs nothing. See [manifest.md](manifest.md#services) for the kinds of service. `deploy --dry-run --compare` runs the build, as `--build` does, then measures on the server what this deployment would change there, and changes nothing: the files its rsync would send or delete (compared by content, a time or a mode alone does not count), the deposited manifest, a missing unit, a Caddy block that differs. A unit edited on the machine is reported and never counted, deploy leaving it as it is without `--force`; a block edited there is refused, as deploy refuses it. `upgrade` asks exactly this of the dashboard and the portal. ## Upgrading the machine `sitesolide upgrade` runs from anywhere, against the machine the configuration names, as the account that deploys: ```console $ sitesolide upgrade --dry-run -> upgrade of deploy@203.0.113.10, zone example.com, to the code of release v0.4.0, dry run: nothing is changed on the machine up to date Caddy's drop-in up to date backups up to date egress proxy out of date steward differs: steward.js; would run bin/deploy-steward.sh dashboard/: sitesolide deploy --dry-run --compare, which builds it first out of date dashboard differs: /srv/sites/dashboard/public: 18 entries to send or delete; would run sitesolide deploy in dashboard/ ... missing installer not installed: sitesolide setup installs it ``` Each component is checked first, reading only: what setup checks for it, and the fingerprint of what would be installed against what is, the SHA-256 the deploy script itself verifies after installing. Only a component that differs is redeployed, through the script or the `sitesolide deploy` setup runs for it, in the order [upgrading.md](upgrading.md) gives. A component the machine does not carry is reported missing, and `sitesolide setup` installs it. A failure stops at its component, quotes the end of what its script printed and the command that shows more; the same command again resumes there. A second run right after a successful one finds everything up to date and changes nothing. It never reads nor rotates a secret, and never touches Caddy but through `bin/deploy-caddy.sh` and `systemctl`. See [upgrading.md](upgrading.md). ## Refreshing the apps' units A project's protections live in its systemd unit, generated from its manifest: the ports it may listen on, the system calls it may make. A unit is written by `sitesolide deploy`, which also sends the folder's code, and `upgrade` never touches a project's unit: after a release that changes what the units carry, every app keeps the previous one until it is deployed again, and an app whose code on the workstation is not what runs in production cannot be, without shipping that code. `sitesolide refresh` writes the units alone, from the manifest the last deployment left on the machine, over the owner's SSH, from any folder: ```console $ sitesolide refresh --all --dry-run -> refresh of deploy@203.0.113.10, zone example.com: every project's units, generated again from the manifest the server carries; no code is sent; dry run, nothing is changed -> blog, static: no unit to refresh -> notes, service unchanged /etc/systemd/system/notes.service -> shop, 2 services to write /etc/systemd/system/shop.service, written by an earlier release unchanged /etc/systemd/system/shop.api.service ... -> would be refreshed: shop already current: notes static, no unit to refresh: blog $ sitesolide refresh --all ``` Run it after an upgrade whose notes say the generated units changed, to give every app the new protections without redeploying its code; `refresh ` does one project. Each project's manifest is read on the machine, `/srv/sites//sitesolide.json`, and validated as deploy validates one; one that does not read, or that this release refuses, refuses its project, saying why, and nothing is changed for it. Its units are generated as deploy generates them, the zone and the contact from the configuration, and each one is decided as deploy decides it without `--force`: a current unit is left alone, a missing one or one an earlier release wrote is written, root's, 0644, and one edited by hand is kept and reported, never replaced: `refresh` has no `--force`, and `sitesolide deploy --force` in the project's folder is what switches such a unit. A main unit edited by hand, in a project of several services, keeps all of them, as deploy refuses that case. No unit is ever removed, a service no longer declared included. When a unit is to be written, the project takes the Caddy lock, the one deploy holds from its first deposit to the end of its restart, and reads its manifest and units again under it: a deployment or the dashboard that changed them meanwhile stops the run there, nothing written. Then `daemon-reload`, and the project's services restarted and checked exactly as deploy does it: every unit but a scheduled service, which its timer starts; each active; the site over HTTPS; every service that runs for good still active ten seconds later, never restarted by systemd meanwhile. A project with nothing to write takes no lock and restarts nothing, and a second run right after the first changes nothing on the machine. A static project has nothing to refresh. `--all` takes the projects one at a time, in the order of their names, and stops at the first whose restart or check fails: one site down at most, its units written and its services restarted under them, the projects after it left as they are. The refusal names the project, what was written for it, what the run did before it and what it never reached. A service that held under its earlier unit and falls under the new one hit one of its protections: its journal, `sitesolide logs` in its folder, says which. The run ends with a summary: refreshed, already current, kept (edited by hand), refused, static. `--dry-run` reads the manifests and the units, and prints for each project the units it would write, with their text, and touches nothing. With `--json`, the `result` carries `dryRun`, `all`, `projects` (each `slug`, `state`: `refreshed`, `outdated` in a dry run, `current`, `kept`, `refused` or `static`; `write`, the units written or that a dry run would write; `current`; `kept`; `reasons`, why it was refused; `url` and `status`, what the site answered), and the slugs by state: `refreshed`, `outdated`, `current`, `kept`, `refused`, `static`. It is the owner's command, like `upgrade`: refused with a token (`needs-ssh`), and not an MCP tool. ## Access to a project Who may open a project, and who may do what on it, is one list per project, its people with access, which the steward keeps: each entry an email or a whole domain, `@acme.com`, with a role, Can open (`can-open`), Viewer, Developer or Admin, each including the ones below it. [access.md](access.md#people-with-access-beside-tokens) says what each role does. `sitesolide share`, in the project's folder, reads the list and changes it; the dashboard's *Access* section is the other way. ```console $ sitesolide share -> access to notes, https://notes.example.com/, over SSH, as the owner general access: Restricted: visitors are asked to sign in. people with access: alice@acme.com Developer @acme.com Can open client@example.org Can open, password access until 2026-11-07 09:00 UTC Also open it without being listed: the owner, and owner@acme.com, set on the server to open every site. $ sitesolide share bob@acme.com carol@acme.com --role developer -> access to notes, https://notes.example.com/, over SSH, as the owner -> bob@acme.com: Developer on notes -> carol@acme.com: Developer on notes ... send: Open https://notes.example.com/ and sign in with your Google account. $ sitesolide share dana@example.net --expires 30d -> access to notes, https://notes.example.com/, over SSH, as the owner -> dana@example.net: Can open, password access until 2026-11-07 09:00 UTC on notes password for dana@example.net: Xith-G4r4-nRJs-uDMV shown once: send it to them yourself, with the address; the machine keeps only its hash ... $ sitesolide share --remove @acme.com -> access to notes, https://notes.example.com/, over SSH, as the owner -> @acme.com no longer has access to notes: refused from their next request ... ``` With no argument it shows the project's general access, its people with access, each with their role and password access with its expiry, and who else opens it without being on the list. Emails and `@domain`s are given access, Can open unless `--role` names another; someone already on the list gets the role named, raised or lowered, and someone who has it already is left as they are. A domain is Can open only, and only once signing in with a company account is set up. Someone outside the company's domains, or anyone when that is not set up, is Can open only, with **password access**: the machine draws a password for them, which the command prints once, and the access lasts what `--expires` says, 7 days by default. `--remove` takes access away, from the person's next request. Several people are handled one by one: a refusal stops before the next, and says who was already given access. Nothing is sent to anyone: after giving someone access with an account, the command prints the line to send, `send:`, and for password access the password, once. Two things are never done here: general access, from the dashboard's *Access* section, and the preview code, `sitesolide lock`. `--domain` and `--only-admins`, from before, are refused with a pointer: a domain is written `@acme.com`. With `--json`, the `result` carries `slug`, `url`, `general` (`public`, `restricted`, `code`, or null for a project not deployed), `entries`, `signIn` (`configured`, `allowedDomains`), `message` (the line to send, null until signing in with a company account is set up), `changed`, and after a change `changes`, each `{ who, change, role }`, `change` being `add`, `role`, `none` or `remove`, and `password` for password access just given: ```console $ sitesolide share dana@example.net --json ... {"type":"result","ok":true,"command":"share","slug":"notes","url":"https://notes.example.com/","general":"restricted","entries":[...,{"who":"dana@example.net","kind":"password","role":"can-open","by":"owner","createdAt":1791450000000,"updatedAt":1791450000000,"password":{"expiresAt":1792054800000,"expired":false}}],"signIn":{"configured":true,"allowedDomains":["acme.com"]},"message":"Open https://notes.example.com/ and sign in with your Google account.","changed":true,"changes":[{"who":"dana@example.net","change":"add","role":"can-open","password":"Xith-G4r4-nRJs-uDMV"}]} ``` The owner's `share` runs over SSH: root on the machine asks the steward on its owner socket, `/run/sitesolide-steward-owner/owner.sock`, which only root opens, the body on standard input so that no address goes through a shell. The steward judges the change by its access rules, writes the portal's projection and its registry, and records it in its journal under `owner`. Root is the owner: no unlock is asked. It touches nothing else, Caddy least of all. With a token, the command goes through the dashboard to the steward, and gives Can open alone: see [access.md](access.md#giving-access-to-what-you-deployed). While the portal on the machine still decides from its own tables, halfway through an upgrade, the command warns of it: `sitesolide upgrade` deploys the portal that reads the registry. ## People Everyone with access, machine-wide, for the owner, from any folder: their roles per project, their password access, who may create projects, the domains, and the admin emails, which open every site as admin, set on the server in `OIDC_ADMIN_EMAILS`. The dashboard's *People* page is the other way. Roles are given per project, with `sitesolide share`; `people` grants the right to create projects. ```console $ sitesolide people -> people of https://dashboard.example.com, over SSH, as the owner alice@acme.com notes: Developer, shop: Viewer client@example.org notes: Can open, password access until 2026-11-07 09:00 UTC you@acme.com no project; every site, as admin: set on the server in OIDC_ADMIN_EMAILS @acme.com Can open: blog, notes the company's domains: acme.com; anyone else gets password access $ sitesolide people alice@acme.com --may-create -> people of https://dashboard.example.com, over SSH, as the owner -> alice@acme.com may create projects, Admin of each one they create: open https://dashboard.example.com and sign in with their company account alice@acme.com notes: Developer, shop: Viewer; may create projects ``` `--may-create` grants the right to create projects to someone who signs in with a company account, whatever their roles, none included: they sign in to the dashboard, mint a token that may create projects, and become Admin of each project it creates. `--no-create` takes it back and leaves their roles; someone then left with no role above Can open no longer signs in to the dashboard, their sessions closed and their tokens revoked. With `--json`, the listing's `result` carries `people` (each `who`, `roles` by project, `can-open` for Can open, `create`, `passwords`, each `slug`, `expiresAt`, `expired`, and `admin`), `domains` (each `slug`, `domain`), `signIn` and `changed: false`; `--may-create` and `--no-create`, `email`, `create`, `roles`, `change` (`create` or `none`) and `changed`. Root on the machine asks the steward on its owner socket, as for `share`, and the steward records the change in its journal under `owner`. With a token, the command is refused before anything is sent: it is the owner's. See [access.md](access.md#people-with-access-beside-tokens) for what each person sees, and [dashboard/README.md](../dashboard/README.md#access) for the machine's side. ## The preview code A site's general access is Public, Restricted or Anyone with the code, chosen from its *Access* section in the dashboard, by its Admin or the owner. From the project's folder, over the owner's SSH, the code is set, replaced and dropped too: ```bash sitesolide lock # Anyone with the code, and the code; on a site that has one, the one in force sitesolide lock --new-code # a new code: the link already sent stops opening the site sitesolide unlock # Public again sitesolide lock --status # what the machine wants, installs and serves, without touching ``` `lock` and `unlock` take the same path as the dashboard, and no other: root on the machine asks the steward on its owner socket, which launches the gatekeeper. The gatekeeper draws the code on the machine, six characters without O, I, 0 or 1, writes the manifest, the codes file and the locks' fragment in one transaction, a restricted site losing its portal in the same one, validates, reloads Caddy with systemctl, checks over HTTPS that the door page answers without the code and the site with it, a replaced code no longer opening it, and puts everything back at the slightest failure. The command prints the code and the link that carries it, once, and the local `sitesolide.json` then says what the machine does, to commit. The steward records the change under `owner`, never the code. A site that serves its own domain closes there too, and the gatekeeper checks both addresses; the link then leads to the domain, where its visitors go. ```console $ sitesolide lock -> general access of notes: Anyone with the code, through the steward and the gatekeeper code set: validated, reloaded, notes.example.com answers the door page's 401 and opens with its code, 13 other site(s) still answer notes opens with its code. Code : K7M2PQ Link : https://notes.example.com/?key=K7M2PQ ``` With `--json`, the `result` carries `slug`, `access` (`code` or `public`), `code` and `url` (null once Public), and `manifestWritten`. ## A folder without a manifest `sitesolide detect` reads the folder and prints the manifest it implies: a Go module, a FastAPI or Flask app, a package.json app or generated site, a folder of files. It reads no machine and needs no configuration. `--write` writes the manifest, never over an existing one. `sitesolide deploy` in such a folder prints the same manifest and stops; `--yes` writes it and deploys, unless the machine already serves a project of that name. An app whose manifest declares no `port` gets a free one from `deploy`, written back into `sitesolide.json` to be committed. See [agents.md](agents.md#zero-configuration) for what is recognised, and what is never decided for you: secrets and the network. ## A machine from a cloud provider `sitesolide machine` orders the VM itself, before there is a configuration to read: it talks to the provider's API, never over SSH. See [machine.md](machine.md) for what it creates, what it costs and what the token needs. ```text sitesolide machine create --provider hetzner --name order a VM, its firewall and your SSH key, ready for setup --type cx23 by default; a refusal lists what the location sells --location fsn1 by default --image debian-13 by default --backups the provider's daily backups, about 20 % on the price --ssh-key ~/.ssh/id_ed25519.pub, id_ecdsa.pub or id_rsa.pub by default sitesolide machine list --provider hetzner the machines sitesolide created, their addresses and price sitesolide machine destroy --provider hetzner delete the machine and its firewall, for good --confirm the name typed back, where no terminal can ask for it --delete-key delete the SSH key uploaded for it too any machine command --token-stdin read the token from standard input rather than HCLOUD_TOKEN --json, on every one of them: one JSON event per line, see docs/agents.md the token is never an option, where ps would show it: see docs/machine.md ``` `create` ends on the command that comes next, `sitesolide setup root@ --zone --email `, once port 22 answers. ## For agents `--json` prints one JSON event per line on standard output, nothing else, and ends with a `result` or an `error` carrying a `hint`. `sitesolide mcp` serves `detect`, `deploy`, `status`, `logs`, `share` (and `access`, its read alone) and `lock --status` as tools to an MCP client; `setup`, `upgrade` and `refresh`, which change the machine itself or every app on it, are not tools. Both are described in [agents.md](agents.md). ## With a token A workstation with no `server`, but the dashboard's address and a token, never touches SSH: someone's own, or an agent's. See [access.md](access.md). ```text sitesolide login --url keep the token, check it --token-stdin read it from standard input sitesolide deploy build here, upload, follow the machine's log sitesolide status the projects this token may deploy sitesolide logs [--follow] the journal of this folder's project --lines how many lines back, 50 by default, 500 at most sitesolide share this folder's project's general access and people with access ... give them Can open: people inside the company's domains, or one of them --remove ... take their Can open away sitesolide secrets this folder's project's secret files and variable names, never a value sitesolide secrets set set a variable of .env, typed at a prompt that does not echo it --stdin the value from standard input, one trailing line break dropped sitesolide secrets remove ... take variables out of .env --restart with set or remove: then restart the service, and say whether it holds sitesolide restart restart this folder's project's service, and say whether it holds --json, on every one of them: one JSON event per line, see docs/agents.md a secret's value never goes on the command line: with --json, secrets set needs --stdin SITESOLIDE_API and SITESOLIDE_TOKEN, both in the environment, win over the files. ``` `login` asks for the token at a prompt that does not echo it, unless `--token-stdin` or `SITESOLIDE_TOKEN` gives it, and keeps it in `~/.config/sitesolide/secrets/team-token`, 0600, the address it checked it against beside it, `team-token-api`, and the address in `config.json` under `api`. `SITESOLIDE_API` and `SITESOLIDE_TOKEN` in the environment win over both, for an agent's sandbox, the two together: the vault's token goes to the address `login` wrote beside it and to no other, and a `SITESOLIDE_API` or a `config.json` naming another is refused, `unauthenticated`, until `sitesolide login --url` checks the token there. A vault filled before `login` wrote the address sends its token to the `api` of the `config.json` beside it. A person's own token says so: `login` and `status` print whose roles bound it, and what it may do is the steward's reading of those roles at that moment. `share` gives Can open alone, and never password access, see [access.md](access.md#giving-access-to-what-you-deployed). `deploy` sends the manifest first, so that a refusal arrives before the build, then a gzip-compressed tar holding `app/` and `public/`, exactly what rsync would have sent, and follows the machine's log until the end; it exits non-zero when the deployment fails. A manifest with a `start` and no `port` gets one chosen on the machine. `secrets` lists the secret files of the project in the folder, with their variable names and whether a restart is pending, and never a value: no route reads one back. `secrets set NAME` and `secrets remove NAME...` change `.env`, the one file a token changes, and only when the deployed `sitesolide.json` declares it; `set` creates the file when it is missing. All three, the listing included, need a token minted with *Manage secrets*. The value never comes from the command line, where the shell's history and the process list would keep it: `set` asks for it at a prompt that does not echo it, or reads all of standard input with `--stdin`, one trailing line break dropped. `--stdin` reads a pipe: when standard input is the terminal, where the value would show as it is typed, it is refused, and leaving it out gives the prompt. At the prompt, Backspace, Ctrl-U and Ctrl-W edit the value, Enter ends it and Ctrl-C abandons it; an arrow key, Tab or any other control key is refused, and so is a paste of several lines, whose rest is read and dropped rather than left for the shell to run. Under `--json` nothing prompts, so `set` needs `--stdin`. What the machine would refuse is refused before a request leaves, and the refusal never quotes what was typed: `NAME=value`, or anything after the name; a name that looks like a value, over 64 characters or starting as a service's key does (`sk_`, `ghp_`, `xox`, `AKIA`, `sst_` and the like), with a reminder that the shell's history now holds it (a variable really named so is set from the dashboard); a value on several lines, with a null byte, not valid Unicode, or over 8 KiB, which belongs in a file read whole, from the dashboard; a value with a Unicode noncharacter, U+FDD0 to U+FDEF or the last two code points of a plane, for which systemd would read none of the file's variables. An empty value is refused too: an unset variable piped by mistake would empty the secret in service; `remove` takes a variable out. When the connection drops once a change has left, the run says it may have been written, rather than that the dashboard is unreachable: `sitesolide secrets` shows the names, and the same command run again makes the same change once more, harmlessly. ```bash sitesolide secrets # files and variable names sitesolide secrets set API_KEY # typed at the prompt, no echo openssl rand -hex 32 | sitesolide secrets set SESSION_SECRET --stdin --restart sitesolide secrets remove OLD_KEY LEGACY_URL sitesolide restart ``` `--restart` restarts the service once the change is written. `restart`, which any token that deploys the project may run, restarts its service and watches it for eight seconds: it exits non-zero unless the service holds. Over the owner's SSH, both point to the dashboard instead: *Secrets* manages the files there, and *Restart service*, in a project's *Overview*, restarts it. See [access.md](access.md#secrets-with-a-token). Any other option is refused before a request leaves, `deploy --dry-run` first: a dry run reads the machine over the owner's SSH access, which a token does not carry, and the control API has no route that judges without deploying. `--json` prints the same events as over SSH, and one final `result` or `error`. The other commands need the owner's SSH access and say so. The owner, whose configuration has a `server`, keeps SSH for every command; `--api` makes one go through the dashboard instead, to see what a token's holder sees. `sitesolide backups` lists the snapshots the machine keeps of the project's data folder, on the server and in the bucket, with the size of each and what it added to the repository when it was taken, and its last run. It reads and changes nothing: a restore is made from the dashboard's *Backups* section, which saves the current data first. See [dashboard/src/backup/README.md](../dashboard/src/backup/README.md). ## Upgrading to the hardened deploy A release of the CLI that closes holes found in review: what an agent, a token or a cloned folder could make `deploy` do. It changes how some deployments behave, listed below. The commands are yours to run, in this order. It came before `sitesolide upgrade`, which now does steps 2 and 3 on its own, only for what differs: see [Upgrading the machine](#upgrading-the-machine). **What changes for you.** | Before | Now | |---|---| | any valid slug | `caddy`, `ssh`, `cron`, `nftables`, `www`, every `systemd-*` and `sitesolide-*`... are refused, and so is a slug systemd already gives to a unit deploy did not write and that does not serve `/srv/sites/`, `--force` or not | | `deploy --dry-run` ran the build | it shows the build and does not run it; `--dry-run --build` runs it | | `install` ran in `app/` as the deployment account, with sudo | it runs as `site-`, in a transient unit with its service's walls: the network but not the loopback, only `app/` writable, `HOME` in a throwaway `/tmp`, 1G and fifteen minutes at most | | ports checked before the build | checked again under the Caddy lock, before the manifest is deposited; 3022, 3026 and 3029 are refused to every project but the dashboard, the portal and analytics | | `.git` sent with the code, `public/` sent as it is | `.git` and every `.env*` never leave, at any depth; the next deploy removes those an earlier one left in `public/`, and Caddy answers 404 for them | | `source` could lead anywhere | from a manifest outside the `sites` repository of your configuration, only inside its own repository | | a token's `deploy --dry-run` deployed for real | refused, as is every option the token's path does not carry; `--json` speaks events there too | **1. The workstation.** Pull, then `bin/test.sh`. Check that `sitesolide deploy --dry-run` in a project with a `build` prints `[dry-run] build (...), not run`, and that `config.json` names your sites repository under `sites` (`sitesolide init --sites ` otherwise): a manifest there keeps a `source` that climbs out, as `mini-lab` does. **2. What the machine runs from this repository**, before redeploying any other site: they recognise the Caddy blocks the new generator writes, whose file server hides `.git` and `.env*`. Until they are updated, the dashboard refuses to change the general access of a site redeployed with the new CLI ("redeploy the site first"), and a token's deploy of it stops on `edited-by-hand`; nothing served changes. ```bash cd dashboard && sitesolide deploy # the dashboard itself, and its own block bin/deploy-steward.sh # policy.ts bin/deploy-gatekeeper.sh # bin/cli/fragment.ts bin/deploy-installer.sh # src/installer/, policy.ts, bin/cli/ ``` Each script checks what it installs. Then the dashboard's *Access* section should switch a test site between Public and Restricted as before. **3. The Caddyfile**, whose landing, wildcard and customer-domain blocks now hide `.git` and `.env*`: ```bash bin/deploy-caddy.sh # validates with systemd's environment, reloads, restores on failure ``` Check: `curl -s -o /dev/null -w '%{http_code}\n' https://./.git/config` answers `404`, and the site's home `200`. **4. Every app, one by one**, with `sitesolide deploy`. Its block is replaced without `--force` ("written by an earlier release, the current one replaces it"), and its `install` now runs as `site-`: the step reads `-> dependencies (...), as site- in its service's walls`. An install that wrote outside `app/`, needed root or a tool installed into `HOME` now fails with `install failed`, the code in place and the service not restarted: move that step into `build`, or install the tool on the machine. The platform's own manifests, the dashboard's and analytics' `bun install --production`, run in `app/` and need nothing more. **5. Leftovers.** A `.env` an earlier deploy put in `app/` stays there, since an excluded file is protected from rsync's `--delete`, and Bun still loads it at start. Read them, then remove the stale ones: ```bash ssh "sudo find /srv/sites/*/app -maxdepth 3 -name '.env*' -not -path '*/node_modules/*'" ``` **Rolling back.** Check out the previous commit on the workstation, and run steps 2 and 3 from it. A site whose block was written by this release then reads, to the previous CLI, as a block that "no longer matches the manifest": `sitesolide deploy --force` in its folder puts the previous block back.