Deploy Sugar#
Run one Sugar process on a host you control, under a service manager, serving an
immutable commit-named release directory on loopback behind an HTTPS proxy. The
repository ships the deployment as two fail-closed scripts: npm run deploy
builds and promotes a release, and npm run rollback returns to the previous
one.
Before you start#
- A Linux host with
git,curl,tar,npm, Node.js 20.9 or newer and GNU coreutils. The cutover usesmv -T, so GNUmvis required. - A dedicated unprivileged
sugaraccount that owns the release tree. - An SSH key for the deploy account that is not passphrase-protected, or is
already loaded into an agent. The scripts run
sshwithBatchMode=yes. - Passwordless
sudofor the deploy account, limited tosystemctlon the two units below and tosudo -u sugar. Every call usessudo -n, so a missing rule fails instead of prompting. - A public HTTPS proxy that terminates TLS, forwards to loopback port 3211, preserves streaming responses and overwrites a client-address header.
- The commit you intend to ship, pushed and reachable from
origin.
Prepare the host once#
The deploy script deploys; it does not provision. Create this layout first:
/opt/sugar/
├── repo/ # bare mirror the operator pushes into
├── releases/<full-commit-sha>/ # one immutable, already-built release each
├── current -> releases/<sha> # what the service unit serves
└── previous -> releases/<sha> # the rollback target
/etc/sugar/sugar.env # environment file, root:sugar, mode 0640
/var/lib/sugar/ # SQLite database and WAL, outside every release
sudo useradd --system --create-home --shell /usr/sbin/nologin sugar
sudo install -d -o sugar -g sugar /opt/sugar /opt/sugar/releases /var/lib/sugar
sudo install -d -o root -g sugar -m 0750 /etc/sugar
sudo -u sugar git init --bare -b main /opt/sugar/repo
Configure two systemd units:
sugar.serviceruns assugarwithWorkingDirectory=/opt/sugar/current,EnvironmentFile=/etc/sugar/sugar.env, and listens on127.0.0.1:3211. The working directory is resolved at start, so a release change takes effect only on restart.sugar-backup.serviceis aType=oneshotunit whoseExecStartis an absolute path outside/opt/sugar/releases. It backs up the database with SQLite's online backup API and exits non-zero on any failure. The deploy reads its exit status and trusts it.
Set at least NODE_ENV, APP_URL, SUGAR_DATA_DIR=/var/lib/sugar and
SUGAR_TRUSTED_CLIENT_IP_HEADER in /etc/sugar/sugar.env. See
configuration.html for the full set.
Warning
SUGAR_DATA_DIRmust never point inside a release directory. Releases are pruned; the database must not be.
Deploy a release#
Point the scripts at your host, then read the plan before the deploy:
export SUGAR_DEPLOY_HOST=deploy@your-host
export SUGAR_DEPLOY_KEY=~/.ssh/your-deploy-key
export SUGAR_DEPLOY_ROOT=/opt/sugar
git push
npm run deploy:dry-run
npm run deploy
--host, --key and --root override the same three values for one run. Add
--keep <n> to change release retention, --no-prune to keep every release, and
a full commit sha as the first argument to deploy a commit other than HEAD.
--dry-run opens no connection and changes nothing.
The deploy runs in this order and stops at the first step it cannot confirm:
- Local preflight — resolve the sha, require a clean working tree, fetch
origin, require the sha to be reachable from a remote-tracking ref.--allow-dirtyoverrides the clean-tree refusal only; the committed sha is still the only thing that ships. - Host preflight — prove the layout, the account, the tools, GNU
mv -T, the sudo rules and both units exist. - Publish — push the commit into
/opt/sugar/repoover the deploy key, then confirm the mirror can produce the object. - Release — extract the commit into
/opt/sugar/releases/<sha>. A directory already marked complete is reused; interrupted build debris is rebuilt. - Build —
npm ci && npm run buildassugar, then require.next/sugar-route-surface.json. - Backup — run
sugar-backup.serviceand require a success whose exit timestamp is newer than the start of this step. - Cutover — record the outgoing release in
previousand repointcurrent, each as one atomic rename. - Restart — restart
sugar.serviceand require it active. - Verify — poll both health endpoints until both pass in the same round.
- Rollback — on a verification failure only: promote
previous, restart and re-verify. - Prune — after a verified deploy only: keep the newest five releases plus
whatever
currentandpreviouspoint at.
Re-running the same deploy is safe. A completed release is not rebuilt, and deploying the release that is already live leaves the rollback target alone.
Important Never remove
scripts/from a release. Next.js resolves the route-surface adapter from it on every command,next startincluded.
Verify#
Both http://127.0.0.1:3211/api/health on the host and the public
https://<your-origin>/api/health must return HTTP 200 with:
| Field | Required value | Meaning |
|---|---|---|
ready |
true |
A compute lane is ready, AUTH_SECRET is set, a configured receipt signer is valid, model and routing-policy configuration is valid, and no critical route is missing. |
routes.missing |
[] |
Every critical route this build should register is servable. |
routes.source |
"build" |
The route manifest could be proved to describe this build. "unavailable" means the check could not run. |
breakers.open may be non-zero and the deploy still passes: a tripped lane is a
degraded provider, not a bad build.
Then send one streaming request through the public proxy and confirm disconnect
and timeout behaviour. To exercise the whole path — bootstrap, provider setup,
key issue, buffered and streamed completion, usage and quota — against a fake
upstream, run npm run smoke. Set SMOKE_PORT and SMOKE_UPSTREAM_PORT above
3050 when the host is already serving Sugar.
Roll back#
npm run rollback
Rollback is automatic when verification fails. Run it by hand when a release
passed its health checks but is misbehaving in a way health cannot see. It
refuses when previous is missing, dangling, identical to current or is not a
built release; it repoints previous at the release being retired, so rolling
forward again is one command; and it requires the same three health conditions
from both endpoints.
Rollback deliberately does not run sugar-backup.service. The backup gate
belongs to the forward path, where new code is about to touch the database.
If it does not work#
| Symptom | Check | Recovery |
|---|---|---|
The deploy stops at backup |
journalctl -u sugar-backup.service |
Nothing changed and the old release is still serving. Fix the backup destination, credentials or file permissions, then deploy again. Do not bypass the gate. |
| The backup reports success with a stale exit time | Whether the unit is RemainAfterExit=yes and its start became a no-op |
Repair the unit so each start actually runs a backup. |
routes.source is "unavailable" |
Whether npm run build wrote .next/sugar-route-surface.json, and whether scripts/ survived into the release |
Rebuild the release from the complete tree. The deploy refuses to promote a build whose route surface cannot be proved. |
| Loopback health passes and public health fails | DNS, TLS and the proxy | Fix the public path. The deploy treats this as a failed deploy and rolls back. |
| The rollback itself fails | systemctl status sugar.service, journalctl -u sugar.service -n 200, ls -l /opt/sugar |
Treat it as an outage and repair by hand. Do not retry the deploy. |
| Sign-in throttles behave as if every visitor shares one address | Whether SUGAR_TRUSTED_CLIENT_IP_HEADER names the header the proxy overwrites |
Set it and restart. Sugar warns at start-up in production when it is unset. |
Related: configuration.html, operations.html, troubleshooting.html.