Update host dir and release process

This commit is contained in:
Max committed 2026-10-28 16:30:36 -07:00
1 parent 42287395fe
commit 2ce1fc073d
2 files changed
+119 -21

No files matched your search

+18 -5
View File
@@ -17,6 +17,13 @@
# /assets/, /sw.js and /manifest.json unresolvable once they needed their # /assets/, /sw.js and /manifest.json unresolvable once they needed their
# own location blocks. # own location blocks.
# #
# It also no longer points into the repo. deploy.sh publishes the built
# client to /var/www/ss/releases/<timestamp> and atomically repoints the
# /var/www/ss/current symlink at it. Pointing nginx at that stable path
# means a deploy is one rename -- nginx resolves the symlink per request,
# so no reload is needed and no request sees a half-copied directory.
# Rollback is the same rename pointing at an older release.
#
# 3. proxyWebsockets on /socket. recommendedProxySettings does NOT add the # 3. proxyWebsockets on /socket. recommendedProxySettings does NOT add the
# Upgrade/Connection headers, so Socket.IO has been silently falling back # Upgrade/Connection headers, so Socket.IO has been silently falling back
# to long-polling -- that's the live note-sync path running degraded. # to long-polling -- that's the live note-sync path running degraded.
@@ -45,7 +52,8 @@ services.nginx = {
virtualHosts."192.168.1.7" = { virtualHosts."192.168.1.7" = {
# serverAliases = [ "notes.example.com" ]; # if the tunnel forwards the public host # serverAliases = [ "notes.example.com" ]; # if the tunnel forwards the public host
root = "REPO_PATH/client/dist"; # Published by deploy.sh; a symlink to the live release. Not in the repo.
root = "/var/www/ss/current";
locations."/" = { locations."/" = {
tryFiles = "$uri $uri/ /index.html"; tryFiles = "$uri $uri/ /index.html";
@@ -84,7 +92,12 @@ services.nginx = {
}; };
}; };
# Note on file access: nginx runs as its own user, so REPO_PATH/client/dist must # Note on file access: nginx runs as its own user and needs read access to the
# be readable by it -- every parent directory needs +x. If the repo lives under # web root, with +x on every parent directory. Serving from /var/www/ss is what
# /home/mab this usually means loosening the home directory's permissions, or # makes that a non-issue -- the one-time `chmod 755 /var/www /var/www/ss` in
# putting the clone somewhere like /srv instead. # deploy.sh is the whole of it. This is the reason the root is not the repo
# clone: a clone under /home/mab would require loosening the permissions on the
# home directory itself.
#
# Uploads are NOT here. Express serves them from REPO_PATH/staticFiles at
# /api/static via the proxy below, so they are untouched by deploys.
+101 -16
View File
@@ -13,24 +13,72 @@
# environment.systemPackages or the user's profile -- NOT only inside a # environment.systemPackages or the user's profile -- NOT only inside a
# nix-shell, or this will fail at the install step) # nix-shell, or this will fail at the install step)
# - pm2 is already managing NoteServer (pm2 resurrect on boot) # - pm2 is already managing NoteServer (pm2 resurrect on boot)
# - nginx serves client/dist with an SPA fallback to index.html # - nginx serves $WEBROOT/current with an SPA fallback to index.html
# #
# nginx is NOT touched here. On NixOS it is declarative -- see configs/. # nginx is NOT touched here. On NixOS it is declarative -- see configs/.
# #
# --- the web root --------------------------------------------------------
#
# The built client is published to /var/www/ss, NOT served out of the repo.
# nginx runs as its own user, so serving from a clone under /home/mab meant
# loosening the home directory's permissions for every parent dir. /var/www is
# the conventional place for this and sidesteps the whole problem.
#
# Layout, which is the standard symlink-swap pattern:
#
# /var/www/ss/
# releases/2026-10-28_1147/ <- one directory per deploy
# releases/2026-10-28_0930/
# current -> releases/2026-10-28_1147
#
# nginx points at the stable path $WEBROOT/current. Publishing is a rename of
# that symlink, which is a single atomic syscall: no request ever sees a
# half-copied directory, and no window exists where the root is missing.
# Rolling back is the same rename pointing at the previous release.
#
# Only the *client* moves. The API server still runs from the repo under pm2,
# and user uploads are still served by Express from $REPO/staticFiles at
# /api/static -- deliberately, so deploys never touch user data.
#
# ONE-TIME SETUP on a new server (needs root):
#
# sudo mkdir -p /var/www/ss/releases
# sudo chown -R "$USER" /var/www/ss
# sudo chmod 755 /var/www /var/www/ss
#
# Override the location for a staging box with: WEBROOT=/var/www/ss-staging ./deploy.sh
#
set -euo pipefail set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$REPO" cd "$REPO"
WEBROOT="${WEBROOT:-/var/www/ss}"
RELEASE="$WEBROOT/releases/$(date +%Y-%m-%d_%H%M%S)"
# Number of old releases to keep around for rollback.
KEEP_RELEASES=5
step(){ printf '\n\033[32m::--:: %s\033[0m\n' "$1"; } step(){ printf '\n\033[32m::--:: %s\033[0m\n' "$1"; }
# --- sanity checks before changing anything ------------------------------ # --- sanity checks before changing anything ------------------------------
for tool in git pnpm pm2; do for tool in git pnpm pm2 rsync; do
command -v "$tool" >/dev/null || { echo "$tool is not on PATH" >&2; exit 1; } command -v "$tool" >/dev/null || { echo "$tool is not on PATH" >&2; exit 1; }
done done
# Fail here rather than after a three-minute build. A missing web root is
# almost always a server that never had the one-time setup above run on it.
if [ ! -d "$WEBROOT/releases" ]; then
echo "$WEBROOT/releases does not exist -- run the one-time setup at the top of this script." >&2
exit 1
fi
if [ ! -w "$WEBROOT/releases" ]; then
echo "$WEBROOT/releases is not writable by $(whoami) -- check the chown in the one-time setup." >&2
exit 1
fi
if [ -n "$(git status --porcelain --untracked-files=no)" ]; then if [ -n "$(git status --porcelain --untracked-files=no)" ]; then
echo "Working tree has local modifications. Commit or stash them first:" >&2 echo "Working tree has local modifications. Commit or stash them first:" >&2
git status --short >&2 git status --short >&2
@@ -54,23 +102,54 @@ pnpm install --frozen-lockfile --dir client
# --- build --------------------------------------------------------------- # --- build ---------------------------------------------------------------
# dist is gitignored, so the server builds it. Built into a temp dir first so a # dist is gitignored, so the server builds it. The build stays inside the repo
# failed build leaves the currently-served files untouched. # and is only copied to $WEBROOT once it has been checked, so a failed build
# cannot touch what is currently being served.
step "Building client..." step "Building client..."
rm -rf client/dist.new rm -rf client/dist
pnpm --dir client exec vite build --outDir dist.new --emptyOutDir pnpm --dir client exec vite build
if [ ! -f client/dist.new/index.html ] || [ ! -f client/dist.new/sw.js ]; then # manifest.json is checked in under client/public and copied verbatim by Vite;
echo "Build output looks wrong (missing index.html or sw.js) -- keeping the previous build." >&2 # if it is missing, the PWA installs without icons, scope or a share target.
rm -rf client/dist.new for required in index.html sw.js manifest.json; do
exit 1 if [ ! -f "client/dist/$required" ]; then
fi echo "Build output looks wrong (missing $required) -- $WEBROOT left untouched." >&2
exit 1
fi
done
step "Swapping in the new build..." # --- publish -------------------------------------------------------------
rm -rf client/dist.old
[ -d client/dist ] && mv client/dist client/dist.old step "Publishing to $RELEASE..."
mv client/dist.new client/dist mkdir -p "$RELEASE"
rm -rf client/dist.old # Trailing slash on the source: copy the *contents* of dist into the release
# dir. --delete is harmless on a fresh dir but keeps a re-run of the same
# timestamp honest.
rsync -a --delete client/dist/ "$RELEASE/"
# nginx reads these as its own user, so they must be world-readable.
chmod -R a+rX "$RELEASE"
step "Pointing $WEBROOT/current at the new release..."
# Two steps because `ln -sfn` over an existing symlink unlinks before relinking,
# which leaves a brief window with no web root at all. Creating a scratch
# symlink and renaming it over the old one is a single atomic rename(2).
ln -sfn "$RELEASE" "$WEBROOT/current.new"
mv -T "$WEBROOT/current.new" "$WEBROOT/current"
# --- prune ---------------------------------------------------------------
# Keep the most recent $KEEP_RELEASES for rollback, drop the rest. Never remove
# whatever `current` actually resolves to, however old it is.
live="$(readlink -f "$WEBROOT/current")"
ls -1d "$WEBROOT"/releases/*/ 2>/dev/null \
| sort -r \
| tail -n +$((KEEP_RELEASES + 1)) \
| while read -r stale; do
[ "$(readlink -f "$stale")" = "$live" ] && continue
echo " pruning $(basename "$stale")"
rm -rf "$stale"
done
# --- restart ------------------------------------------------------------- # --- restart -------------------------------------------------------------
@@ -83,4 +162,10 @@ pm2 reload "$REPO/server/ecosystem.config.js" --env production --update-env
pm2 save pm2 save
step "Done." step "Done."
echo " serving $(readlink -f "$WEBROOT/current")"
echo
echo " To roll back, repoint the symlink at the previous release:"
echo " ls $WEBROOT/releases"
echo " ln -sfn $WEBROOT/releases/<older> $WEBROOT/current.new && mv -T $WEBROOT/current.new $WEBROOT/current"
echo
pm2 list pm2 list