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>
This commit is contained in:
parent
819f8fa91c
commit
6f656b7121
6 changed files with 177 additions and 158 deletions
|
|
@ -1,23 +1,25 @@
|
|||
# Production environment for bifrost-portal.fenja.ai
|
||||
# Copy to the EnvironmentFile path referenced by the systemd unit
|
||||
# (default: /etc/bifrost-portal.env) and fill in real values. Keep it
|
||||
# chmod 600, owned by the service user. NEVER commit the real file.
|
||||
# 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 OUTSIDE the deploy dir so
|
||||
# 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=/var/lib/bifrost-portal/bifrost.db
|
||||
BIFROST_DB_PATH=/opt/fenja/data/bifrost-portal/bifrost.db
|
||||
|
||||
# Absolute path to the runtime uploads dir (event photos). Also outside the
|
||||
# deploy dir. Honored by src/lib/uploads.ts.
|
||||
BIFROST_UPLOAD_DIR=/var/lib/bifrost-portal/uploads
|
||||
# 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. 4321 is the dev port; 4322 keeps
|
||||
# us clear of it. Verify nothing else on the box uses 4322 (see DEPLOY.md).
|
||||
# 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
|
||||
|
||||
|
|
|
|||
194
DEPLOY.md
194
DEPLOY.md
|
|
@ -1,165 +1,171 @@
|
|||
# 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, coexisting with the other Fenja site.
|
||||
nginx terminates TLS and reverse-proxies the `bifrost-portal.fenja.ai` hostname
|
||||
to the app on `127.0.0.1:4322`. Data is a single SQLite file plus an uploads dir.
|
||||
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) |
|
||||
| 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 | `/var/lib/bifrost-portal/` (`bifrost.db`, `uploads/`, `backups/`) |
|
||||
| Service user | `bifrost` (unprivileged) |
|
||||
| Persistent data | `/opt/fenja/data/bifrost-portal/` (`bifrost.db`, `uploads/`, `backups/`) |
|
||||
| Service user | `fenja` (existing) |
|
||||
| systemd unit | `bifrost-portal.service` |
|
||||
| Env file | `/etc/bifrost-portal.env` (chmod 600) |
|
||||
| Env file | `/opt/bifrost-portal/.env` (chmod 600) |
|
||||
| Server | Ubuntu 24.04, x86_64, nginx 1.24.0, certbot 2.9.0 |
|
||||
|
||||
Artifacts referenced below all live in this repo: `deploy/bifrost-portal.service`,
|
||||
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. Pre-flight — confirm the port is free
|
||||
## 0. Toolchain — upgrade Node to 22, enable pnpm
|
||||
|
||||
Another Fenja app already runs on this box. Make sure nothing holds `4322`:
|
||||
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
|
||||
sudo ss -ltnp | grep ':4322' || echo "4322 is free"
|
||||
# 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 it's taken, pick another loopback port and change it in **both** `/etc/bifrost-portal.env`
|
||||
(`PORT=`) and `deploy/nginx/bifrost-portal.fenja.ai.conf` (`proxy_pass`).
|
||||
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 VPS IP, same target as the existing
|
||||
Fenja site. Confirm before requesting a cert:
|
||||
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. One-time server provisioning
|
||||
|
||||
Run as a sudo-capable user. Assumes Node 22, pnpm, nginx, sqlite3, certbot, git
|
||||
are already present (the existing Fenja app implies most are).
|
||||
## 2. Provision dirs (reuse the `fenja` user)
|
||||
|
||||
```bash
|
||||
# Service user (no login shell, no home spam)
|
||||
sudo useradd --system --create-home --home-dir /var/lib/bifrost-portal \
|
||||
--shell /usr/sbin/nologin bifrost
|
||||
|
||||
# Persistent data dirs
|
||||
sudo install -d -o bifrost -g bifrost /var/lib/bifrost-portal
|
||||
sudo install -d -o bifrost -g bifrost /var/lib/bifrost-portal/uploads
|
||||
sudo install -d -o bifrost -g bifrost /var/lib/bifrost-portal/backups
|
||||
|
||||
# Code dir
|
||||
sudo install -d -o bifrost -g bifrost /opt/bifrost-portal
|
||||
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
|
||||
```
|
||||
|
||||
Clone the repo into the code dir (uses the same `git.fenja.ai` remote this repo
|
||||
already points at — make sure the `bifrost` user, or the user running deploys,
|
||||
has a deploy key that can read it):
|
||||
## 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):
|
||||
|
||||
```bash
|
||||
sudo -u bifrost git clone ssh://git@git.fenja.ai:2222/joh/project-bifrost-platform.git \
|
||||
sudo -u fenja git clone ssh://git@git.fenja.ai:2222/joh/project-bifrost-platform.git \
|
||||
/opt/bifrost-portal
|
||||
```
|
||||
|
||||
## 3. Environment file
|
||||
## 4. Environment file
|
||||
|
||||
```bash
|
||||
sudo cp /opt/bifrost-portal/.env.production.example /etc/bifrost-portal.env
|
||||
sudo chown bifrost:bifrost /etc/bifrost-portal.env
|
||||
sudo chmod 600 /etc/bifrost-portal.env
|
||||
# Generate the session secret and paste it in as BIFROST_SECRET:
|
||||
openssl rand -hex 32
|
||||
sudo nano /etc/bifrost-portal.env
|
||||
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
|
||||
```
|
||||
|
||||
Make sure `BIFROST_DB_PATH`, `BIFROST_UPLOAD_DIR`, `HOST`, `PORT`, `NODE_ENV`
|
||||
match the table above. `BIFROST_SECRET` is what signs sessions and invite
|
||||
tokens — rotating it later logs everyone out and invalidates pending invites.
|
||||
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.
|
||||
|
||||
## 4. First build + database
|
||||
## 5. First build + database
|
||||
|
||||
```bash
|
||||
cd /opt/bifrost-portal
|
||||
sudo -u bifrost pnpm install --frozen-lockfile # rebuilds better-sqlite3 for this arch
|
||||
sudo -u bifrost pnpm build
|
||||
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 bifrost --preserve-env=BIFROST_DB_PATH \
|
||||
bash -c 'set -a; source /etc/bifrost-portal.env; set +a; node scripts/migrate.js'
|
||||
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 bifrost \
|
||||
bash -c 'set -a; source /etc/bifrost-portal.env; set +a; pnpm db:seed:production'
|
||||
# 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 and must be built **on the server** for its CPU
|
||||
> arch (ARM on a Hetzner CAX11). That's why we `pnpm install` here rather than
|
||||
> copying `node_modules`. `scripts/deploy.sh` does this on every deploy.
|
||||
> `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.
|
||||
|
||||
## 5. systemd service
|
||||
## 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 # should be active (running)
|
||||
curl -fsS http://127.0.0.1:4322/login >/dev/null && echo "app responding"
|
||||
sudo systemctl status bifrost-portal --no-pager
|
||||
curl -fsS http://127.0.0.1:4322/login >/dev/null && echo "app responding on 4322"
|
||||
```
|
||||
|
||||
Allow the `bifrost` user to restart just this unit without a password (used by
|
||||
`deploy.sh`):
|
||||
Let `fenja` restart just this unit without a password (used by `deploy.sh`):
|
||||
|
||||
```bash
|
||||
echo 'bifrost ALL=(root) NOPASSWD: /usr/bin/systemctl restart bifrost-portal, /usr/bin/systemctl status bifrost-portal' \
|
||||
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
|
||||
```
|
||||
|
||||
## 6. nginx + TLS
|
||||
## 7. nginx + TLS
|
||||
|
||||
```bash
|
||||
sudo cp /opt/bifrost-portal/deploy/nginx/bifrost-portal.fenja.ai.conf \
|
||||
/etc/nginx/sites-available/
|
||||
sudo ln -s /etc/nginx/sites-available/bifrost-portal.fenja.ai.conf \
|
||||
/etc/nginx/sites-enabled/
|
||||
/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 edits the file to wire up the cert paths):
|
||||
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
|
||||
```
|
||||
|
||||
Verify, then optionally enable HSTS (uncomment the `Strict-Transport-Security`
|
||||
line in the nginx conf and reload) once you're happy HTTPS is solid:
|
||||
|
||||
```bash
|
||||
curl -fsSI https://bifrost-portal.fenja.ai/login | head -n1
|
||||
```
|
||||
|
||||
## 7. Nightly backups
|
||||
> 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 bifrost crontab -e
|
||||
sudo -u fenja crontab -e
|
||||
# add:
|
||||
15 3 * * * /opt/bifrost-portal/scripts/backup.sh >> /var/log/bifrost-backup.log 2>&1
|
||||
15 3 * * * /opt/bifrost-portal/scripts/backup.sh >> /opt/fenja/data/bifrost-portal/backup.log 2>&1
|
||||
```
|
||||
|
||||
`backup.sh` writes 30-day-retained `.backup` snapshots to
|
||||
`/var/lib/bifrost-portal/backups`. Per SPEC §7.3, add a second cron line to sync
|
||||
that dir to the Hetzner Storage Box (rclone/rsync) for offsite copies.
|
||||
`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.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -168,36 +174,38 @@ that dir to the Hetzner Storage Box (rclone/rsync) for offsite copies.
|
|||
After pushing to `master` on `git.fenja.ai`:
|
||||
|
||||
```bash
|
||||
sudo -u bifrost bash -c 'cd /opt/bifrost-portal && ./scripts/deploy.sh'
|
||||
sudo -u fenja bash -c 'cd /opt/bifrost-portal && ./scripts/deploy.sh'
|
||||
```
|
||||
|
||||
It pulls, installs (rebuilding native deps), builds, migrates, and restarts.
|
||||
The DB and uploads are untouched.
|
||||
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 bifrost git log --oneline -n 10 # find the good commit
|
||||
sudo -u bifrost bash -c 'BRANCH=<good-sha> ./scripts/deploy.sh' # or: git reset --hard <sha> then deploy
|
||||
sudo -u fenja git log --oneline -n 10
|
||||
sudo -u fenja bash -c 'BRANCH=<good-sha> ./scripts/deploy.sh'
|
||||
```
|
||||
|
||||
Schema migrations are forward-only — a code rollback past a migration may need a
|
||||
DB restore. To restore a backup (stop the app first):
|
||||
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 bifrost bash -c 'gunzip -c /var/lib/bifrost-portal/backups/bifrost-YYYYMMDD-HHMMSS.db.gz > /var/lib/bifrost-portal/bifrost.db'
|
||||
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 not running or wrong port. `systemctl status bifrost-portal`,
|
||||
`journalctl -u bifrost-portal -n 50`, and re-check `PORT` matches `proxy_pass`.
|
||||
- **App starts then exits** — usually a missing/invalid `/etc/bifrost-portal.env`
|
||||
or unwritable `BIFROST_DB_PATH`. Check `journalctl -u bifrost-portal`.
|
||||
- **`better-sqlite3` errors on boot** — native module built for the wrong arch.
|
||||
Re-run `pnpm install --frozen-lockfile` on the server and rebuild.
|
||||
- **Migrations hit the wrong DB** — confirm the env file is sourced; `migrate.js`
|
||||
honors `BIFROST_DB_PATH` (verified) and falls back to the repo-local dev db otherwise.
|
||||
- **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`.
|
||||
|
|
|
|||
|
|
@ -1,37 +1,49 @@
|
|||
# systemd unit for the Bifrost portal (bifrost-portal.fenja.ai)
|
||||
# Install to /etc/systemd/system/bifrost-portal.service
|
||||
# Then: sudo systemctl daemon-reload && sudo systemctl enable --now bifrost-portal
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 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).
|
||||
#
|
||||
# Assumes:
|
||||
# - code checkout at /opt/bifrost-portal (built in place: dist/server/entry.mjs)
|
||||
# - environment file at /etc/bifrost-portal.env (chmod 600, see .env.production.example)
|
||||
# - a dedicated unprivileged service user `bifrost`
|
||||
# - persistent data under /var/lib/bifrost-portal (db + uploads)
|
||||
# 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, Node standalone)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
Description=Bifrost portal (Astro SSR)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=bifrost
|
||||
Group=bifrost
|
||||
User=fenja
|
||||
Group=fenja
|
||||
WorkingDirectory=/opt/bifrost-portal
|
||||
EnvironmentFile=/etc/bifrost-portal.env
|
||||
EnvironmentFile=/opt/bifrost-portal/.env
|
||||
ExecStart=/usr/bin/node /opt/bifrost-portal/dist/server/entry.mjs
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
RestartSec=5
|
||||
|
||||
# Hardening — the service only needs to read its code and write its data dir.
|
||||
# stdout / stderr → journald
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=bifrost-portal
|
||||
|
||||
# ─── Hardening (matches the other Fenja units) ───
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/var/lib/bifrost-portal
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,54 +1,55 @@
|
|||
# nginx site for bifrost-portal.fenja.ai
|
||||
# Reverse-proxies to the Bifrost portal Node server on 127.0.0.1:4322.
|
||||
# Coexists with other Fenja sites on this box — it only claims this hostname.
|
||||
# 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:
|
||||
# sudo cp deploy/nginx/bifrost-portal.fenja.ai.conf /etc/nginx/sites-available/
|
||||
# sudo ln -s /etc/nginx/sites-available/bifrost-portal.fenja.ai.conf /etc/nginx/sites-enabled/
|
||||
# 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: obtain the cert first (see DEPLOY.md). Either run
|
||||
# sudo certbot --nginx -d bifrost-portal.fenja.ai
|
||||
# (certbot edits this file in place), OR issue with --webroot and keep the
|
||||
# 443 block below as-is. The :80 block must exist before certbot runs.
|
||||
# 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).
|
||||
|
||||
# HTTP — ACME challenge + redirect everything else to HTTPS.
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
# HTTPS — terminates TLS, proxies to the Node app.
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
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;
|
||||
# Modern TLS defaults (Mozilla "intermediate"). If certbot manages this
|
||||
# file it may append its own ssl_* includes — harmless duplicates aside.
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_prefer_server_ciphers off;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
|
||||
# Event photo uploads can be a few MB; keep headroom above the app's limit.
|
||||
client_max_body_size 12m;
|
||||
|
||||
# Security headers. HSTS only after you've confirmed HTTPS works end-to-end.
|
||||
# ─── 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;
|
||||
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" 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;
|
||||
|
|
@ -57,10 +58,6 @@ server {
|
|||
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_set_header X-Forwarded-Host $host;
|
||||
# Upgrade headers in case any route uses them; harmless otherwise.
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,17 +4,17 @@
|
|||
# (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 `bifrost` user):
|
||||
# Install as a cron job (as the `fenja` user):
|
||||
# crontab -e
|
||||
# 15 3 * * * /opt/bifrost-portal/scripts/backup.sh >> /var/log/bifrost-backup.log 2>&1
|
||||
# 15 3 * * * /opt/bifrost-portal/scripts/backup.sh >> /opt/fenja/data/bifrost-portal/backup.log 2>&1
|
||||
#
|
||||
# Per SPEC §7.3 the offsite target is a Hetzner Storage Box; sync BACKUP_DIR
|
||||
# there separately (e.g. rclone/rsync in a second cron line).
|
||||
# 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:-/var/lib/bifrost-portal/bifrost.db}"
|
||||
BACKUP_DIR="${BACKUP_DIR:-/var/lib/bifrost-portal/backups}"
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ set -euo pipefail
|
|||
APP_DIR="${APP_DIR:-/opt/bifrost-portal}"
|
||||
SERVICE="${SERVICE:-bifrost-portal}"
|
||||
BRANCH="${BRANCH:-master}"
|
||||
ENV_FILE="${ENV_FILE:-/etc/bifrost-portal.env}"
|
||||
ENV_FILE="${ENV_FILE:-/opt/bifrost-portal/.env}"
|
||||
|
||||
cd "$APP_DIR"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue