Files
events-hub/deployment.md
2026-10-27 12:15:26 +02:00

362 lines
11 KiB
Markdown

# Deploying the Hub
How to run the Hub on the server at `events-hub.crosscode.co.za`, behind FASTPANEL's nginx and an
frp tunnel.
```
Internet ──► frps (public VPS) ══ tunnel ══► frpc (this server, /etc/frp/frpc.ini)
│
▼
nginx (FASTPANEL, :80 / :443, HTTPS)
├── /api/* ──► backend (Express, 127.0.0.1:5200)
└── /* ──► frontend (Next.js, 127.0.0.1:3200)
│
PostgreSQL (localhost:5432)
```
Only nginx is reachable from outside. Ports 5200 and 3200 must **not** be added to frpc.
Replace `<siteuser>` below with the system user FASTPANEL creates for the site.
---
## 1. Prerequisites (once per server)
- Node 20+ (`node -v`), and PM2: `sudo npm install -g pm2`
- PostgreSQL 14+
- Git access to this repo
If Hope Events is already deployed on this server, you already have all of these.
---
## 2. DNS
Add an `A` record for `events-hub.crosscode.co.za` pointing at the **public IP of the frps VPS**,
not this server's IP. Check it with `dig +short events-hub.crosscode.co.za`.
---
## 3. frp
Open `/etc/frp/frpc.ini`. How much you need to do depends on how it already forwards web traffic.
**a) Plain TCP forwarding of 80 and 443** — you'll see something like:
```ini
[web-http]
type = tcp
local_ip = 127.0.0.1
local_port = 80
remote_port = 80
[web-https]
type = tcp
local_ip = 127.0.0.1
local_port = 443
remote_port = 443
```
Then **you don't need to change anything**: nginx picks the site by domain name. Go to step 4.
**b) Per-domain `http` / `https` proxies** (`custom_domains = ...`, with `vhost_http_port` /
`vhost_https_port` set on frps) — add a pair for the Hub:
```ini
[events-hub-http]
type = http
local_ip = 127.0.0.1
local_port = 80
custom_domains = events-hub.crosscode.co.za
[events-hub-https]
type = https
local_ip = 127.0.0.1
local_port = 443
custom_domains = events-hub.crosscode.co.za
```
Use `https` (passthrough), not `https2http`: TLS is ended by nginx on this server with the
FASTPANEL certificate.
Check the file and restart:
```bash
sudo frpc verify -c /etc/frp/frpc.ini
sudo systemctl restart frpc
sudo journalctl -u frpc -n 30 --no-pager # look for "start proxy success"
```
Port 80 must reach nginx through the tunnel, or Let's Encrypt can't issue the certificate.
---
## 4. Database
Create a database and user. In FASTPANEL: **Databases → Create**, choose PostgreSQL, name
`events_hub`. Or from the shell:
```bash
sudo -u postgres psql <<'SQL'
CREATE USER events_hub WITH PASSWORD 'a-long-random-password';
CREATE DATABASE events_hub OWNER events_hub;
SQL
```
Your connection string is then
`postgresql://events_hub:a-long-random-password@localhost:5432/events_hub`.
(URL-encode the password if it contains `@ : / ? #`.)
---
## 5. Site in FASTPANEL
1. **Sites → Create site**, domain `events-hub.crosscode.co.za`. Note the site's system user
(`<siteuser>`) and folder, e.g. `/var/www/<siteuser>/data/www/events-hub.crosscode.co.za`.
2. **SSL certificates → Let's Encrypt** for the domain. Turn on the redirect from HTTP to HTTPS.
3. Leave the nginx config for step 8; the apps need to be running first.
---
## 6. Get the code and configure it
Work as the site user so files end up with the right owner:
```bash
sudo -iu <siteuser>
cd /var/www/<siteuser>/data/www/events-hub.crosscode.co.za
git clone <repo-url> app
cd app
```
### Backend
```bash
cd backend
cp .env.example .env
nano .env
```
Set at least:
| Variable | Value |
|---|---|
| `DATABASE_URL` | the connection string from step 4 |
| `FRONTEND_URL` | `https://events-hub.crosscode.co.za` |
| `JWT_SECRET` | `openssl rand -base64 48` |
| `HUB_ENCRYPTION_KEY` | `openssl rand -base64 48`. **Never change this** once installs have registered |
| `HUB_SIGNING_KEY_FILE` | `./keys/hub-signing-key.pem` (the default) |
| `TRUST_PROXY` | `1` |
| `SMTP_*`, `ALERT_EMAIL` | your mail settings, so alerts get sent |
```bash
chmod 600 .env
npm ci
npx prisma migrate deploy && npx prisma generate
npm run generate-keys # only on the very first deploy; it refuses to overwrite
npm run create-admin -- --email you@example.com --name "Your Name"
```
`generate-keys` prints the **public key**. Put it into Hope Events
(`backend/src/hub/publicKey.js`) and release. Copy `keys/hub-signing-key.pem` somewhere safe and
offline **now** (see step 11).
### Frontend
`NEXT_PUBLIC_API_URL` is built into the frontend at build time, so set it **before** building:
```bash
cd ../frontend
echo "NEXT_PUBLIC_API_URL=https://events-hub.crosscode.co.za" > .env.local
npm ci
npm run build
```
Don't add `/api` to that URL: the frontend already calls paths like `/api/auth/login`.
---
## 7. Run with PM2
Still as `<siteuser>`, from the `app` folder:
```bash
pm2 start npm --name events-hub-backend --cwd backend -- start
pm2 start npm --name events-hub-frontend --cwd frontend -- start
pm2 save
```
The names are different from Hope Events' `backend` / `frontend` processes, so the two don't
clash on the same server.
Start on boot (run once; it prints a `sudo` command to paste):
```bash
pm2 startup systemd
```
Check:
```bash
pm2 status
pm2 logs events-hub-backend --lines 20 # expect "[hub] Listening on 5200"
curl -s http://127.0.0.1:5200/api/health # {"ok":true}
curl -sI http://127.0.0.1:3200 | head -1 # HTTP/1.1 200 OK
```
The backend refuses to start (and says why in the logs) if `DATABASE_URL`, `JWT_SECRET`,
`HUB_ENCRYPTION_KEY` or the signing key is missing.
Run **one** backend process only (no `-i max` / cluster mode): the background alert jobs run
in-process and would fire once per copy.
---
## 8. nginx (FASTPANEL)
In FASTPANEL, open the site → **Configuration files** (the nginx config of the site). Inside the
`server { ... }` block that listens on 443, replace the default `location /` (the one serving
static files / PHP) with:
```nginx
location /api/ {
proxy_pass http://127.0.0.1:5200;
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;
}
location / {
proxy_pass http://127.0.0.1:3200;
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;
}
```
Leave the `listen`, `server_name`, `ssl_certificate` and `/.well-known/acme-challenge/` parts
that FASTPANEL wrote alone, so certificate renewal keeps working. If FASTPANEL also generated
an Apache backend or PHP-FPM for this site, turn it off in the site settings. The Hub doesn't
use it.
Save (FASTPANEL tests the config and reloads nginx). From the shell, you can check with:
```bash
sudo nginx -t && sudo systemctl reload nginx
```
---
## 9. Check it from outside
```bash
curl -s https://events-hub.crosscode.co.za/api/health # {"ok":true}
```
Then open `https://events-hub.crosscode.co.za`, log in, and set up two-factor (required on
first login). In **Settings**, check that the public key shown matches the one in Hope Events.
Point Hope Events installs at the Hub with `HUB_URL=https://events-hub.crosscode.co.za` in their
`backend/.env`.
---
## 10. Real client IPs through frp (recommended)
With frp in front, nginx sees every connection as coming from frpc (`127.0.0.1`). So the
backend thinks **all** visitors share one IP. That has two effects:
- The login rate limit (20 tries / 15 min) counts everyone together. Someone hammering the login
page also locks you out.
- Audit log entries all show `127.0.0.1`.
(The heartbeat limit is 30/min per IP, and installs call every 15 minutes, so heartbeats are fine
up to a few hundred installs, even sharing one IP.)
You can fix this with the PROXY protocol, which carries the real client IP through the tunnel.
**This applies to every site on this nginx**: once a `listen` has `proxy_protocol`, *all* server
blocks on that port must have it, and nothing can reach nginx on that port except through frp.
Only do it if all your sites come in through frp.
1. In each web proxy section of `/etc/frp/frpc.ini` (80 and 443), add:
```ini
proxy_protocol_version = v2
```
2. In nginx, add `proxy_protocol` to **every** `listen` on those ports. With FASTPANEL, that
means every site's config, e.g. `listen 443 ssl proxy_protocol;`. Then add this once, in
the `http {}` block (e.g. a file in `/etc/nginx/conf.d/realip.conf`):
```nginx
set_real_ip_from 127.0.0.1;
real_ip_header proxy_protocol;
```
3. `sudo nginx -t && sudo systemctl reload nginx && sudo systemctl restart frpc`
After that, `$remote_addr` is the real visitor, and with `TRUST_PROXY=1` the backend sees it too.
If you'd rather not touch the other sites, skip this. The Hub works without it.
---
## 11. Backups
Two things must survive the server: **the database** and **the signing key** (plus `.env`,
because `HUB_ENCRYPTION_KEY` can't be recreated).
- **Signing key and `.env`**: copy `backend/keys/hub-signing-key.pem` and `backend/.env` to an
offline place (password manager, encrypted USB) once. They don't change.
- **Database, daily**: if FASTPANEL's backup feature covers PostgreSQL for this site, turn it on.
Otherwise, as `<siteuser>`, `crontab -e`:
```cron
30 2 * * * pg_dump "postgresql://events_hub:PASSWORD@localhost:5432/events_hub" -Fc -f "$HOME/backups/events_hub-$(date +\%a).dump"
```
(`mkdir -p ~/backups` first). That keeps one dump per weekday. Copy them off the server
regularly.
To restore a dump: `pg_restore --clean --no-owner -d "<DATABASE_URL>" events_hub-Mon.dump`
---
## 12. Updating
```bash
sudo -iu <siteuser>
cd /var/www/<siteuser>/data/www/events-hub.crosscode.co.za/app
git pull
cd backend
npm ci
npx prisma migrate deploy && npx prisma generate
pm2 restart events-hub-backend
cd ../frontend
npm ci
npm run build
pm2 restart events-hub-frontend
```
Installs keep working while the Hub is briefly down (licences are valid offline for 30 days).
---
## Troubleshooting
| Symptom | Look at |
|---|---|
| Site doesn't load at all | `systemctl status frpc`, `journalctl -u frpc`; DNS points at the frps VPS; frps allows ports 80/443 |
| Let's Encrypt fails | Port 80 isn't reaching nginx through frp (step 3) |
| 502 Bad Gateway | `pm2 status`, `pm2 logs events-hub-backend` / `events-hub-frontend` |
| Login page loads, but login fails with a network/CORS error | `FRONTEND_URL` in `backend/.env` (exact `https://` origin, no trailing slash); `NEXT_PUBLIC_API_URL` in `frontend/.env.local`, **then rebuild** the frontend |
| "Too many attempts" for everyone | Shared IP through frp, see step 10 |
| Installs fail signature checks | nginx must pass the body through unchanged. Don't add body-rewriting modules to `/api/` |
| Backend exits on start | `pm2 logs events-hub-backend`: it names the missing variable or key |