project-bifrost-platform/DEPLOY.md
Arlind 6f656b7121 chore(deploy): align deploy artifacts to the target server's conventions
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>
2026-06-17 13:16:57 +02:00

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-sqlite3 is native; pnpm install builds/fetches it for this machine. scripts/deploy.sh re-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, check PORT matches the proxy_pass.
  • App starts then exits — bad/missing /opt/bifrost-portal/.env or an unwritable BIFROST_DB_PATH. journalctl -u bifrost-portal.
  • better-sqlite3 errors on boot — native module built for the wrong Node ABI. Re-run pnpm install --frozen-lockfile and restart.
  • Migrations hit the wrong DB — make sure the .env is sourced; migrate.js honors BIFROST_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, restart fenja + bifrost-customer.