Deploying an update
Use the script, not the steps — and the steps that look optional and are not.
./bin/deploy.sh # deploy
./bin/deploy.sh --dry-run # show what would be sent and removed, change nothingUse the script rather than the steps. This procedure has been reconstructed from memory more than once, and each reconstruction dropped a different step. None of the omissions failed loudly; they were found later, by someone wondering why the box was in a state nobody had chosen.
Overridable with DEPLOY_HOST and DEPLOY_ROOT, which is also how you would deploy a second one.
The steps that look optional and are not
rm -rf node_modules before installing. The deploy ends with pnpm prune --prod. A later
pnpm install --frozen-lockfile then reports "Already up to date" and does not restore
devDependencies, so the next build fails on a missing Vite.
Removing node_modules also removes pnpm's shims, so use corepack pnpm rather than pnpm
from that point on, or the very next line is pnpm: command not found.
rsync --delete, over the whole tree. Syncing a list of paths leaves behind files deleted or
renamed in the repository. A stale module that still resolves is an hour of debugging.
rsync --no-owner --no-group. Plain -a carries the developer machine's uid across, so the
controller's code ends up owned by a number that means nothing there. The chown at the end used
to be what corrected this, which meant every deploy fought itself and a failure between the two
steps left the wrong answer in place. Not sending ownership removes the race; the chown stays as
the thing that states the intended result rather than repairs an unintended one.
chown -R root:pve-agents. The service user must be able to read its code and must not
own it.
Migrate before starting, not after. The controller applies migrations lazily, on the first request that touches the database. Skip this and it reports itself healthy on the old schema and surfaces the failure as a broken request rather than a failed deploy.
Stop the service before syncing. Otherwise the tree is replaced under a running process.
Open the site afterwards
check, typecheck, test and build do not load a page. All four have passed against a build
whose client bundle threw during hydration and rendered nothing but an error box. The script cannot
catch this; a person has to look. Anything touching src/router.tsx, routing or SSR especially.
Dependencies are pinned exactly
Because the deploy deletes node_modules every time, a floating version specifier means a deploy
can install something other than what last worked, with nothing in the diff to show for it.
@tanstack/react-router and @tanstack/react-start were once "latest". A fresh install
eventually paired them with @tanstack/react-router-with-query, which had stopped at 1.130 while
the router went on to 1.170 and still declared a peer range of ">=1.43.2". It called an API the
router no longer had, hydration threw, and the whole UI was an error box. Treat a wide peer range
as no guarantee at all.
Last updated on