Generate a dedicated ed25519 deploy key on the server (private key stays put), register the public half read-only, and clone via a bifrost-portal-git SSH alias with IdentitiesOnly so it can't clash with the existing apps' keys. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9.2 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. Deploy key + clone
The fenja user needs read access to this repo on git.fenja.ai. Use a
dedicated, repo-scoped deploy key generated on the server (private key never
leaves the box) plus an SSH alias so it's used only for this repo.
# Generate the keypair (no passphrase — it's for unattended deploys)
sudo -u fenja install -d -m 700 /home/fenja/.ssh
sudo -u fenja ssh-keygen -t ed25519 -N "" \
-f /home/fenja/.ssh/bifrost_portal_deploy \
-C "bifrost-portal-deploy@$(hostname)"
# SSH alias so git uses THIS key only for the portal repo
sudo -u fenja tee -a /home/fenja/.ssh/config >/dev/null <<'EOF'
Host bifrost-portal-git
HostName git.fenja.ai
Port 2222
User git
IdentityFile ~/.ssh/bifrost_portal_deploy
IdentitiesOnly yes
EOF
sudo -u fenja chmod 600 /home/fenja/.ssh/config
sudo -u fenja bash -c 'ssh-keyscan -p 2222 git.fenja.ai >> /home/fenja/.ssh/known_hosts 2>/dev/null'
# Print the PUBLIC key to register
sudo -u fenja cat /home/fenja/.ssh/bifrost_portal_deploy.pub
Upload that public key on git.fenja.ai: Repo → Settings → Deploy Keys →
Add Deploy Key, read-only (leave write access off). Then test and clone:
sudo -u fenja ssh -T bifrost-portal-git # expect a greeting, no password prompt
sudo -u fenja git clone bifrost-portal-git:joh/project-bifrost-platform.git /opt/bifrost-portal
Keep the git checkouts separate. This portal (
project-bifrost-platform) and the existing apps (/opt/fenja,/opt/bifrost-customer) are independent git projects with their own remotes./opt/bifrost-portalis a self-contained checkout — never nest it inside another app's tree, never point its remote at theirs, and only ever runscripts/deploy.shfrom inside/opt/bifrost-portal. Cloning via thebifrost-portal-gitalias makesoriginresolve through the dedicated deploy key, whichscripts/deploy.shuses transparently.
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.