Recon of the live box (Ubuntu 24.04 x86_64, nginx 1.24, certbot 2.9) showed established conventions from the existing fenja / bifrost-customer services. Match them so the portal looks like a first-class citizen: - service runs as the existing `fenja` user, journald logging + full hardening block (ProtectKernelModules, LockPersonality), ExecStart on /usr/bin/node (box upgraded globally to Node 22) - code in /opt/bifrost-portal, in-dir .env (EnvironmentFile), data under the shared /opt/fenja/data/bifrost-portal (ReadWritePaths) - nginx: 1.24 `listen ... ssl http2` syntax, certbot options-ssl-nginx + dhparam includes, server_tokens off, sites-available/bifrost-portal (no .conf) symlinked; 12m body size for photo uploads; port 4322 (free) - deploy.sh / backup.sh point at the new paths - DEPLOY.md rewritten as a server-specific runbook incl. the global Node 22 upgrade + retest of the existing apps, and pnpm via corepack Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.6 KiB
Deploying the Bifrost portal — bifrost-portal.fenja.ai
This app runs as a Node standalone SSR server (Astro @astrojs/node) behind
the existing nginx on the Fenja VPS, alongside the fenja and
bifrost-customer apps. It follows the conventions already established on that
box (verified by inspection): fenja service user, systemd + journald, code in
/opt/<app>, data under /opt/fenja/data, in-dir .env, certbot TLS.
You run every step here. Nothing in this repo touches the live server on its own.
| Thing | Value |
|---|---|
| Hostname | bifrost-portal.fenja.ai |
| App bind | 127.0.0.1:4322 (loopback only; 3000/3001 are the existing apps) |
| Code | /opt/bifrost-portal (git checkout, built in place) |
| Persistent data | /opt/fenja/data/bifrost-portal/ (bifrost.db, uploads/, backups/) |
| Service user | fenja (existing) |
| systemd unit | bifrost-portal.service |
| Env file | /opt/bifrost-portal/.env (chmod 600) |
| Server | Ubuntu 24.04, x86_64, nginx 1.24.0, certbot 2.9.0 |
Repo artifacts referenced below: deploy/bifrost-portal.service,
deploy/nginx/bifrost-portal.fenja.ai.conf, .env.production.example,
scripts/deploy.sh, scripts/backup.sh.
0. Toolchain — upgrade Node to 22, enable pnpm
The box currently has Node v20 at /usr/bin/node, shared by the running
fenja and bifrost-customer services. We're upgrading it globally to 22.
⚠️ This moves those two live apps onto Node 22 as well. Restart and smoke-test them right after (last line of this step). Have a moment of downtime tolerance.
# Upgrade the NodeSource apt repo to the 22.x channel and install:
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
node -v # expect v22.x
# pnpm via corepack (bundled with Node 22) — no separate install:
sudo corepack enable
corepack enable pnpm
pnpm -v # expect a pnpm 9.x/10.x shim
# Move the existing apps onto Node 22 and confirm they still work:
sudo systemctl restart fenja bifrost-customer
sudo systemctl status fenja bifrost-customer --no-pager
curl -fsS https://project-bifrost.fenja.ai/ >/dev/null && echo "existing site OK"
If either existing app misbehaves on 22, that's the risk we accepted — roll the
NodeSource repo back to setup_20.x and reinstall to recover them.
1. DNS
Add an A/AAAA record bifrost-portal → the same VPS IP as project-bifrost.
Confirm before requesting a cert:
dig +short bifrost-portal.fenja.ai
2. Provision dirs (reuse the fenja user)
# Code dir
sudo install -d -o fenja -g fenja /opt/bifrost-portal
# Persistent data under the shared, service-writable tree
sudo install -d -o fenja -g fenja /opt/fenja/data/bifrost-portal
sudo install -d -o fenja -g fenja /opt/fenja/data/bifrost-portal/uploads
sudo install -d -o fenja -g fenja /opt/fenja/data/bifrost-portal/backups
3. Clone the repo
The fenja user needs read access to git.fenja.ai (a deploy key on its
account, or your forwarded agent for the first clone):
sudo -u fenja git clone ssh://git@git.fenja.ai:2222/joh/project-bifrost-platform.git \
/opt/bifrost-portal
4. Environment file
sudo -u fenja cp /opt/bifrost-portal/.env.production.example /opt/bifrost-portal/.env
sudo chmod 600 /opt/bifrost-portal/.env
openssl rand -hex 32 # paste as BIFROST_SECRET
sudo -u fenja nano /opt/bifrost-portal/.env
Confirm BIFROST_DB_PATH, BIFROST_UPLOAD_DIR, HOST, PORT, NODE_ENV
match the table above. BIFROST_SECRET signs sessions and invite tokens —
rotating it later logs everyone out and invalidates pending invites.
5. First build + database
cd /opt/bifrost-portal
sudo -u fenja pnpm install --frozen-lockfile # builds native better-sqlite3 for this box
sudo -u fenja pnpm build
# Create + migrate the production DB at BIFROST_DB_PATH:
sudo -u fenja bash -c 'set -a; source /opt/bifrost-portal/.env; set +a; node scripts/migrate.js'
# Seed the real pilot data (ONE TIME only — skip on later deploys):
sudo -u fenja bash -c 'set -a; source /opt/bifrost-portal/.env; set +a; pnpm db:seed:production'
better-sqlite3is native;pnpm installbuilds/fetches it for this machine.scripts/deploy.shre-runs install on every deploy, so an arch/Node change is always reconciled.
6. systemd service
sudo cp /opt/bifrost-portal/deploy/bifrost-portal.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now bifrost-portal
sudo systemctl status bifrost-portal --no-pager
curl -fsS http://127.0.0.1:4322/login >/dev/null && echo "app responding on 4322"
Let fenja restart just this unit without a password (used by deploy.sh):
echo 'fenja ALL=(root) NOPASSWD: /usr/bin/systemctl restart bifrost-portal, /usr/bin/systemctl status bifrost-portal' \
| sudo tee /etc/sudoers.d/bifrost-portal
sudo chmod 440 /etc/sudoers.d/bifrost-portal
7. nginx + TLS
sudo cp /opt/bifrost-portal/deploy/nginx/bifrost-portal.fenja.ai.conf \
/etc/nginx/sites-available/bifrost-portal
sudo ln -s /etc/nginx/sites-available/bifrost-portal \
/etc/nginx/sites-enabled/bifrost-portal
sudo nginx -t && sudo systemctl reload nginx # :80 block live for ACME
Issue the certificate (certbot wires the cert paths into the file):
sudo certbot --nginx -d bifrost-portal.fenja.ai
sudo nginx -t && sudo systemctl reload nginx
curl -fsSI https://bifrost-portal.fenja.ai/login | head -n1
The site config already includes the
Strict-Transport-Security(HSTS) header to match the existing site. If you want to verify HTTPS end-to-end first, comment that line, reload, confirm, then re-enable.
8. Nightly backups
sudo -u fenja crontab -e
# add:
15 3 * * * /opt/bifrost-portal/scripts/backup.sh >> /opt/fenja/data/bifrost-portal/backup.log 2>&1
backup.sh writes 30-day-retained online .backup snapshots to
/opt/fenja/data/bifrost-portal/backups. Add a second cron line to sync that
dir offsite (rclone/rsync) if you want off-box copies.
Ongoing deploys
After pushing to master on git.fenja.ai:
sudo -u fenja bash -c 'cd /opt/bifrost-portal && ./scripts/deploy.sh'
Pulls, installs (rebuilding native deps), builds, migrates, restarts. The DB and
uploads in /opt/fenja/data/bifrost-portal are untouched.
Rollback
cd /opt/bifrost-portal
sudo -u fenja git log --oneline -n 10
sudo -u fenja bash -c 'BRANCH=<good-sha> ./scripts/deploy.sh'
Migrations are forward-only — a rollback past a migration may need a DB restore (stop the app first):
sudo systemctl stop bifrost-portal
sudo -u fenja bash -c 'gunzip -c /opt/fenja/data/bifrost-portal/backups/bifrost-YYYYMMDD-HHMMSS.db.gz > /opt/fenja/data/bifrost-portal/bifrost.db'
sudo systemctl start bifrost-portal
Troubleshooting
- 502 from nginx — app down or wrong port.
systemctl status bifrost-portal,journalctl -u bifrost-portal -n 50, checkPORTmatches theproxy_pass. - App starts then exits — bad/missing
/opt/bifrost-portal/.envor an unwritableBIFROST_DB_PATH.journalctl -u bifrost-portal. better-sqlite3errors on boot — native module built for the wrong Node ABI. Re-runpnpm install --frozen-lockfileand restart.- Migrations hit the wrong DB — make sure the
.envis sourced;migrate.jshonorsBIFROST_DB_PATH(verified) and otherwise falls back to a repo-local db. - Existing apps broke after the Node 22 upgrade — roll NodeSource back to
setup_20.x,apt-get install -y nodejs, restartfenja+bifrost-customer.