project-bifrost-platform/DEPLOY.md
Arlind d27ab4c98b docs(deploy): document the repo-scoped deploy key + SSH alias flow
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>
2026-06-17 14:02:18 +02:00

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-portal is a self-contained checkout — never nest it inside another app's tree, never point its remote at theirs, and only ever run scripts/deploy.sh from inside /opt/bifrost-portal. Cloning via the bifrost-portal-git alias makes origin resolve through the dedicated deploy key, which scripts/deploy.sh uses 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-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.