Skip to content

Commit 3ac7e95

Browse files
deploy: retarget for melvin.me's real topology (haproxy, separate 0.0.219 instance)
Recon showed melvin.me is already a live JSS 0.0.204 pod (pm2, port 5420, shared /home/ubuntu/jss with losos.org) behind haproxy. So the plugins land as an ISOLATED new pm2 app on port 5480 with their own pinned 0.0.219 — no touch to the shared install or the live sites — fronted at forge.melvin.me via one haproxy backend. Swapped the nginx assumption for haproxy-forge.cfg (nginx kept as alt); ecosystem env and README updated (subdomain, own data root, git 2.43 confirmed OK).
1 parent 8fb224b commit 3ac7e95

3 files changed

Lines changed: 97 additions & 25 deletions

File tree

deploy/README.md

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,36 @@
1-
# Deploying JSS + plugins to a public host (pm2 + nginx)
1+
# Deploying JSS + plugins to a public host (pm2 + haproxy)
22

33
The runbook for putting this repo on a box like `melvin.me`. The code needs
44
**no build step** — deploying is `git pull` + `pm2 restart`. The care is all
55
in three things: which plugins you expose, keeping the public Host consistent,
66
and persisting the data root.
77

8+
## Where this lands on melvin.me (as-recon'd 2026-07-12)
9+
10+
The box already runs, under pm2, a **separate JSS pod per site**`melvin.me`
11+
(`jss start --port 5420 --root /home/ubuntu/www/melvin.me --single-user …
12+
--idp-issuer https://melvin.me/ --git --activitypub --nostr --webrtc --tunnel
13+
--pay`) and `losos.org`, both off the shared `/home/ubuntu/jss` install, which
14+
is **JSS 0.0.204**. haproxy on :80/:443 routes by hostname to each backend.
15+
16+
Two consequences:
17+
18+
1. **The plugins need JSS ≥ 0.0.219** (serverInfo/reservePath/mountApp/
19+
api.plugins live there; the forge won't run on 0.0.204). Do **not** upgrade
20+
the shared install to get them — that would move melvin.me *and* losos.org
21+
15 patch versions at once. Instead this repo's `npm ci` brings its **own**
22+
pinned 0.0.219 in its own `node_modules`, and runs as a **new, isolated pm2
23+
app** (`forge`, port 5480) with zero effect on the live 0.0.204 pods.
24+
2. **Mount it as a subdomain**`forge.melvin.me` → haproxy backend
25+
`127.0.0.1:5480`. A path split (`melvin.me/forge`) collides with the
26+
existing pod's own `/api`, `/oauth`, `/.well-known/*`. See
27+
`haproxy-forge.cfg`.
28+
29+
The box is otherwise ready: Ubuntu 24.04, node 22, **git 2.43** (≥2.38 so forge
30+
merges work), `git-http-backend` present, pm2 6.
31+
32+
## Prerequisites (check on the box first)
33+
834
## Prerequisites (check on the box first)
935

1036
```bash
@@ -23,13 +49,13 @@ pm2 -v # process manager
2349
## 1. Get the code
2450

2551
```bash
26-
sudo mkdir -p /var/lib/jss && sudo chown ubuntu:ubuntu /var/lib/jss # PERSISTENT data root
52+
mkdir -p /home/ubuntu/forge-data # PERSISTENT data root (own to ubuntu)
2753
cd /home/ubuntu
2854
git clone https://github.com/JavaScriptSolidServer/plugins.git
29-
cd plugins && git checkout gh-pages && npm ci
55+
cd plugins && git checkout gh-pages && npm ci # brings its OWN jss 0.0.219
3056
```
3157

32-
The data root (`/var/lib/jss`) holds pods, forge repos, issue indexes, and the
58+
The data root (`/home/ubuntu/forge-data`) holds pods, forge repos, issue indexes, and the
3359
**Blocktrails trail private keys**. Keep it OUTSIDE the checkout (so `git pull`
3460
never touches it) and back it up. Losing the trail keys orphans your anchors.
3561

@@ -38,30 +64,36 @@ never touches it) and back it up. Losing the trail keys orphans your anchors.
3864
JSS mints WebIDs against `idpIssuer` (`https://melvin.me`) and checks pod
3965
ownership against the **request Host**. Behind a TLS proxy this means two rules:
4066

41-
1. **nginx passes `Host` through unchanged**`proxy_set_header Host $host;`
42-
(in the supplied conf). A rewritten Host = 403 on users' own writes.
67+
1. **haproxy passes `Host` through unchanged** — in `mode http` it does by
68+
default; just don't add `http-request set-header Host …`. A rewritten Host
69+
= 403 on users' own writes.
4370
2. **The server's own loopback presents the public Host.** `serve.prod.js`
4471
sets every plugin's `loopbackUrl` to `PUBLIC_URL`, so add a hosts entry so
4572
that self-call resolves locally instead of hair-pinning out to the internet:
4673

4774
```bash
48-
echo "127.0.0.1 melvin.me" | sudo tee -a /etc/hosts
75+
echo "127.0.0.1 forge.melvin.me" | sudo tee -a /etc/hosts
4976
```
5077

51-
Now a plugin's loopback to `https://melvin.me/...` hits local nginx →
52-
JSS, presenting `Host: melvin.me`, and pod-owning writes pass WAC.
78+
Now a plugin's loopback to `https://forge.melvin.me/...` hits local haproxy
79+
→ the forge instance, presenting `Host: forge.melvin.me`, and pod-owning
80+
writes pass WAC.
81+
82+
## 3. haproxy
5383

54-
## 3. nginx
84+
Add the backend + ACL from `deploy/haproxy-forge.cfg` to your existing
85+
`/etc/haproxy/haproxy.cfg` (subdomain `forge.melvin.me``127.0.0.1:5480`),
86+
then:
5587

5688
```bash
57-
sudo cp deploy/nginx-melvin.me.conf /etc/nginx/sites-available/melvin.me
58-
sudo ln -sf /etc/nginx/sites-available/melvin.me /etc/nginx/sites-enabled/
59-
# ensure this line exists in http{} (nginx.conf) for WebSocket upgrades:
60-
# map $http_upgrade $connection_upgrade { default upgrade; '' close; }
61-
sudo nginx -t && sudo systemctl reload nginx
89+
sudo haproxy -c -f /etc/haproxy/haproxy.cfg # validate
90+
sudo systemctl reload haproxy
6291
```
6392

64-
TLS cert via certbot if you don't have one: `sudo certbot --nginx -d melvin.me`.
93+
Needs a cert covering `forge.melvin.me` (a `*.melvin.me` wildcard, or add one
94+
to the crt-list) and a DNS record `forge.melvin.me → this box`.
95+
(`nginx-melvin.me.conf` is kept in this dir as an alternative if you ever move
96+
off haproxy.)
6597

6698
## 4. pm2
6799

@@ -76,14 +108,14 @@ pm2 logs jss # watch it come up
76108
## 5. Verify
77109

78110
```bash
79-
curl -s https://melvin.me/dashboard/ -o /dev/null -w '%{http_code}\n' # 200
80-
curl -s https://melvin.me/forge/ -o /dev/null -w '%{http_code}\n' # 200
111+
curl -s https://forge.melvin.me/dashboard/ -o /dev/null -w '%{http_code}\n' # 200
112+
curl -s https://forge.melvin.me/forge/ -o /dev/null -w '%{http_code}\n' # 200
81113
# register a pod, then confirm a pod-owning write round-trips (the Host check):
82-
# open https://melvin.me/forge/ , sign in, open an issue — 200, not 403.
114+
# open https://forge.melvin.me/forge/ , sign in, open an issue — 200, not 403.
83115
```
84116

85-
Point Phanpy (or any Mastodon client) at `https://melvin.me` to exercise the
86-
fediverse surface.
117+
Point Phanpy (or any Mastodon client) at `https://forge.melvin.me` to exercise
118+
the fediverse surface. (Your existing `melvin.me` pod is untouched.)
87119

88120
## Updating
89121

deploy/ecosystem.config.cjs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
module.exports = {
1717
apps: [
1818
{
19-
name: 'jss',
19+
// A SEPARATE instance from the existing melvin.me/losos.org pods. It
20+
// brings its own pinned JSS 0.0.219 (this repo's node_modules), so it
21+
// does NOT touch the shared /home/ubuntu/jss install (v0.0.204) that
22+
// serves the live sites. New name, new port, new data root.
23+
name: 'forge',
2024
script: 'deploy/serve.prod.js',
2125
cwd: '/home/ubuntu/plugins', // the checkout of JavaScriptSolidServer/plugins
2226
interpreter: 'node',
@@ -27,9 +31,11 @@ module.exports = {
2731
kill_timeout: 5000, // give fastify.close() room on SIGINT
2832
env: {
2933
NODE_ENV: 'production',
30-
PUBLIC_URL: 'https://melvin.me',
31-
DATA: '/var/lib/jss', // PERSISTENT — pods, forge repos, Blocktrails keys
32-
PORT: '3240',
34+
// Subdomain origin (recommended) — its own IdP/pods, no collision with
35+
// the melvin.me pod's own /api, /.well-known, etc.
36+
PUBLIC_URL: 'https://forge.melvin.me',
37+
DATA: '/home/ubuntu/forge-data', // PERSISTENT — pods, forge repos, Blocktrails keys
38+
PORT: '5480', // haproxy forge_be → 127.0.0.1:5480 (5420 is the melvin.me pod)
3339
BIND: '127.0.0.1',
3440
// METRICS_TOKEN: 'set-a-long-random-string-to-expose-/metrics',
3541
// ADMIN_AGENTS: 'https://melvin.me/melvin/profile/card#me',

deploy/haproxy-forge.cfg

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# haproxy snippet to front the JSS + plugins instance on melvin.me's box.
2+
# The box already runs haproxy on :80/:443 routing by hostname to per-site
3+
# backends (melvin.me → :5420, losos.org → …). This adds ONE more backend for
4+
# the plugins instance WITHOUT touching the existing sites.
5+
#
6+
# RECOMMENDED: a subdomain (forge.melvin.me) — its own origin, no path rew[]
7+
# collisions with the melvin.me pod's own /api, /.well-known, etc.
8+
#
9+
# Merge these into the existing haproxy.cfg (usually /etc/haproxy/haproxy.cfg),
10+
# then: sudo haproxy -c -f /etc/haproxy/haproxy.cfg && sudo systemctl reload haproxy
11+
12+
# --- in your existing `frontend https-in` (the ssl-terminating frontend) ------
13+
# Requires a cert covering forge.melvin.me (wildcard *.melvin.me, or add one
14+
# to the crt-list). haproxy preserves the Host header in mode http by default
15+
# — do NOT add `http-request set-header Host …`, or you reintroduce the
16+
# pod-ownership 403.
17+
acl host_forge hdr(host) -i forge.melvin.me
18+
use_backend forge_be if host_forge
19+
20+
# --- new backend --------------------------------------------------------------
21+
backend forge_be
22+
mode http
23+
option forwardfor
24+
http-request set-header X-Forwarded-Proto https
25+
# git packs + media uploads: let big/streamed bodies through.
26+
timeout server 300s
27+
server forge1 127.0.0.1:5480 check
28+
29+
# ---- PATH alternative (melvin.me/forge) — NOT recommended --------------------
30+
# The forge shares fixed roots with the existing melvin.me pod (/api, /oauth,
31+
# /.well-known/*), so a path split would collide and mis-route. If you must do
32+
# path-based, route ONLY /forge, /dashboard, /gallery to forge_be and accept
33+
# that fediverse/identity endpoints stay on the existing pod. A subdomain
34+
# avoids all of this.

0 commit comments

Comments
 (0)