Compare commits
8 commits
096c9bc297
...
0a62984e91
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a62984e91 | |||
| 9d0326e3ea | |||
| cf534777af | |||
| d27ab4c98b | |||
| 01b7106a22 | |||
| 6f656b7121 | |||
| 819f8fa91c | |||
| fab927884c |
10 changed files with 478 additions and 4 deletions
26
.env.production.example
Normal file
26
.env.production.example
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Production environment for bifrost-portal.fenja.ai
|
||||||
|
# Copy to /opt/bifrost-portal/.env on the server and fill in real values.
|
||||||
|
# Keep it chmod 600, owned by fenja:fenja. NEVER commit the real file.
|
||||||
|
# (Matches the existing apps' convention of an in-dir .env loaded via
|
||||||
|
# EnvironmentFile in the systemd unit.)
|
||||||
|
|
||||||
|
# Long random string used to sign sessions and invite tokens.
|
||||||
|
# Generate with: openssl rand -hex 32
|
||||||
|
BIFROST_SECRET=change-me-openssl-rand-hex-32
|
||||||
|
|
||||||
|
# Absolute path to the SQLite database. Lives under the shared /opt/fenja/data
|
||||||
|
# tree (the only path the service may write to) and OUTSIDE the deploy dir, so
|
||||||
|
# redeploys never touch it. Honored by src/lib/db.ts and scripts/migrate.js.
|
||||||
|
BIFROST_DB_PATH=/opt/fenja/data/bifrost-portal/bifrost.db
|
||||||
|
|
||||||
|
# Absolute path to the runtime uploads dir (event photos). Honored by
|
||||||
|
# src/lib/uploads.ts.
|
||||||
|
BIFROST_UPLOAD_DIR=/opt/fenja/data/bifrost-portal/uploads
|
||||||
|
|
||||||
|
# Bind address + port for the Node standalone server. Loopback only — nginx
|
||||||
|
# is the only thing that should reach it. 4322 is free on this box (3000/3001
|
||||||
|
# are the existing fenja / bifrost-customer apps).
|
||||||
|
HOST=127.0.0.1
|
||||||
|
PORT=4322
|
||||||
|
|
||||||
|
NODE_ENV=production
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -3,6 +3,7 @@ node_modules/
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
!.env.production.example
|
||||||
.astro/
|
.astro/
|
||||||
*.db
|
*.db
|
||||||
*.db-shm
|
*.db-shm
|
||||||
|
|
|
||||||
247
DEPLOY.md
Normal file
247
DEPLOY.md
Normal file
|
|
@ -0,0 +1,247 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dig +short bifrost-portal.fenja.ai
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Provision dirs (reuse the `fenja` user)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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`.
|
||||||
49
deploy/bifrost-portal.service
Normal file
49
deploy/bifrost-portal.service
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Systemd unit for the Bifrost portal (bifrost-portal.fenja.ai).
|
||||||
|
# Mirrors the conventions of the existing fenja.service / bifrost-customer.service
|
||||||
|
# on this box: runs as the `fenja` user, logs to journald, writes only to
|
||||||
|
# /opt/fenja/data. Astro SSR standalone server (dist/server/entry.mjs).
|
||||||
|
#
|
||||||
|
# Install to: /etc/systemd/system/bifrost-portal.service
|
||||||
|
#
|
||||||
|
# sudo cp deploy/bifrost-portal.service /etc/systemd/system/bifrost-portal.service
|
||||||
|
# sudo systemctl daemon-reload
|
||||||
|
# sudo systemctl enable --now bifrost-portal
|
||||||
|
# sudo systemctl status bifrost-portal
|
||||||
|
# sudo journalctl -u bifrost-portal -f
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Bifrost portal (Astro SSR)
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=fenja
|
||||||
|
Group=fenja
|
||||||
|
WorkingDirectory=/opt/bifrost-portal
|
||||||
|
EnvironmentFile=/opt/bifrost-portal/.env
|
||||||
|
ExecStart=/usr/bin/node /opt/bifrost-portal/dist/server/entry.mjs
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
# stdout / stderr → journald
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=bifrost-portal
|
||||||
|
|
||||||
|
# ─── Hardening (matches the other Fenja units) ───
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectKernelModules=true
|
||||||
|
ProtectControlGroups=true
|
||||||
|
RestrictSUIDSGID=true
|
||||||
|
LockPersonality=true
|
||||||
|
# Only the shared data dir is writable (db, uploads, backups live here)
|
||||||
|
ReadWritePaths=/opt/fenja/data
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
63
deploy/nginx/bifrost-portal.fenja.ai.conf
Normal file
63
deploy/nginx/bifrost-portal.fenja.ai.conf
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
# nginx site for bifrost-portal.fenja.ai
|
||||||
|
# Reverse-proxies to the Bifrost portal Node server on 127.0.0.1:4322.
|
||||||
|
# Coexists with the existing project-bifrost.fenja.ai site; only claims this
|
||||||
|
# hostname. Matches that site's conventions (nginx 1.24, certbot TLS includes).
|
||||||
|
#
|
||||||
|
# Install (no .conf extension, to match the existing sites-available layout):
|
||||||
|
# sudo cp 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
|
||||||
|
#
|
||||||
|
# TLS: the :80 block must be live first so certbot's ACME challenge succeeds.
|
||||||
|
# Then: sudo certbot --nginx -d bifrost-portal.fenja.ai (edits this file).
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name bifrost-portal.fenja.ai;
|
||||||
|
|
||||||
|
# Let certbot's renewals reach .well-known/acme-challenge on port 80
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Everything else goes to HTTPS
|
||||||
|
location / {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
listen [::]:443 ssl http2;
|
||||||
|
server_name bifrost-portal.fenja.ai;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/bifrost-portal.fenja.ai/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/bifrost-portal.fenja.ai/privkey.pem;
|
||||||
|
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||||
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||||
|
|
||||||
|
# ─── Security headers ───
|
||||||
|
# HSTS — confirm the cert + redirect loop is solid before relying on it.
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
add_header X-Content-Type-Options nosniff always;
|
||||||
|
add_header X-Frame-Options SAMEORIGIN always;
|
||||||
|
add_header Referrer-Policy strict-origin-when-cross-origin always;
|
||||||
|
|
||||||
|
# Event photo uploads can be a few MB (the existing site uses 32k — this
|
||||||
|
# site accepts image uploads, so it needs headroom).
|
||||||
|
client_max_body_size 12m;
|
||||||
|
|
||||||
|
# Don't leak nginx version
|
||||||
|
server_tokens off;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:4322;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -34,8 +34,5 @@
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22"
|
"node": ">=22"
|
||||||
},
|
|
||||||
"pnpm": {
|
|
||||||
"onlyBuiltDependencies": ["better-sqlite3", "esbuild", "sharp"]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
9
pnpm-workspace.yaml
Normal file
9
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# pnpm 10+ stopped reading build-script settings from package.json's "pnpm"
|
||||||
|
# field and blocks dependency build scripts by default. pnpm 11 reads the
|
||||||
|
# allow-list from `allowBuilds` here (captured via `pnpm approve-builds`).
|
||||||
|
# Without it, better-sqlite3's native binary is never compiled and the SSR
|
||||||
|
# server crashes at runtime (ERR_DLOPEN_FAILED / NODE_MODULE_VERSION mismatch).
|
||||||
|
allowBuilds:
|
||||||
|
better-sqlite3: true
|
||||||
|
esbuild: true
|
||||||
|
sharp: true
|
||||||
32
scripts/backup.sh
Executable file
32
scripts/backup.sh
Executable file
|
|
@ -0,0 +1,32 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Nightly SQLite backup for the Bifrost portal. Uses the online .backup API
|
||||||
|
# (safe while the app is running — consistent, no locking issues with WAL).
|
||||||
|
# Keeps 30 days of compressed snapshots.
|
||||||
|
#
|
||||||
|
# Install as a cron job (as the `fenja` user):
|
||||||
|
# crontab -e
|
||||||
|
# 15 3 * * * /opt/bifrost-portal/scripts/backup.sh >> /opt/fenja/data/bifrost-portal/backup.log 2>&1
|
||||||
|
#
|
||||||
|
# For offsite copies, sync BACKUP_DIR to remote storage separately
|
||||||
|
# (e.g. rclone/rsync in a second cron line).
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DB_PATH="${BIFROST_DB_PATH:-/opt/fenja/data/bifrost-portal/bifrost.db}"
|
||||||
|
BACKUP_DIR="${BACKUP_DIR:-/opt/fenja/data/bifrost-portal/backups}"
|
||||||
|
RETENTION_DAYS="${RETENTION_DAYS:-30}"
|
||||||
|
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
|
||||||
|
stamp="$(date -u +%Y%m%d-%H%M%S)"
|
||||||
|
out="$BACKUP_DIR/bifrost-$stamp.db"
|
||||||
|
|
||||||
|
echo "==> Backing up $DB_PATH -> $out"
|
||||||
|
sqlite3 "$DB_PATH" ".backup '$out'"
|
||||||
|
gzip -f "$out"
|
||||||
|
|
||||||
|
echo "==> Pruning backups older than $RETENTION_DAYS days"
|
||||||
|
find "$BACKUP_DIR" -name 'bifrost-*.db.gz' -type f -mtime "+$RETENTION_DAYS" -delete
|
||||||
|
|
||||||
|
echo "==> Backup complete: ${out}.gz"
|
||||||
48
scripts/deploy.sh
Executable file
48
scripts/deploy.sh
Executable file
|
|
@ -0,0 +1,48 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Server-side deploy for the Bifrost portal. Run ON THE VPS, as the `bifrost`
|
||||||
|
# service user, from inside the checkout (/opt/bifrost-portal).
|
||||||
|
#
|
||||||
|
# cd /opt/bifrost-portal && ./scripts/deploy.sh
|
||||||
|
#
|
||||||
|
# Pulls latest, installs deps (rebuilding the native better-sqlite3 for this
|
||||||
|
# box's arch), builds, migrates, and restarts the service. Idempotent and
|
||||||
|
# safe to re-run. Does NOT touch the database file or uploads — those live in
|
||||||
|
# /var/lib/bifrost-portal and persist across deploys.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
APP_DIR="${APP_DIR:-/opt/bifrost-portal}"
|
||||||
|
SERVICE="${SERVICE:-bifrost-portal}"
|
||||||
|
BRANCH="${BRANCH:-master}"
|
||||||
|
ENV_FILE="${ENV_FILE:-/opt/bifrost-portal/.env}"
|
||||||
|
|
||||||
|
cd "$APP_DIR"
|
||||||
|
|
||||||
|
echo "==> Loading $ENV_FILE for migrate (BIFROST_DB_PATH)"
|
||||||
|
set -a; # shellcheck disable=SC1090
|
||||||
|
source "$ENV_FILE"; set +a
|
||||||
|
|
||||||
|
echo "==> Fetching origin/$BRANCH"
|
||||||
|
git fetch --prune origin
|
||||||
|
git checkout "$BRANCH"
|
||||||
|
git reset --hard "origin/$BRANCH"
|
||||||
|
|
||||||
|
echo "==> Installing dependencies (frozen lockfile)"
|
||||||
|
# pnpm rebuilds better-sqlite3 for this machine's arch via onlyBuiltDependencies.
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
echo "==> Building"
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
echo "==> Applying database migrations -> $BIFROST_DB_PATH"
|
||||||
|
node scripts/migrate.js
|
||||||
|
|
||||||
|
echo "==> Restarting $SERVICE"
|
||||||
|
sudo systemctl restart "$SERVICE"
|
||||||
|
|
||||||
|
echo "==> Waiting for health"
|
||||||
|
sleep 2
|
||||||
|
sudo systemctl --no-pager --lines=0 status "$SERVICE"
|
||||||
|
|
||||||
|
echo "==> Deploy complete: $(git rev-parse --short HEAD)"
|
||||||
|
|
@ -5,7 +5,9 @@ import { join, dirname } from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const dbPath = join(__dirname, '..', 'bifrost.db');
|
// Honor BIFROST_DB_PATH in production so migrations hit the same file the
|
||||||
|
// running app uses (see src/lib/db.ts). Falls back to the repo-local dev db.
|
||||||
|
const dbPath = process.env.BIFROST_DB_PATH ?? join(__dirname, '..', 'bifrost.db');
|
||||||
const migrationsDir = join(__dirname, '..', 'migrations');
|
const migrationsDir = join(__dirname, '..', 'migrations');
|
||||||
|
|
||||||
const db = new Database(dbPath);
|
const db = new Database(dbPath);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue