1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
|
# fund-lab production deployment (systemd --user)
Loopback-only services behind the existing `cloudflared` tunnel. The tunnel
ingress already maps `fund.somhairle.bid -> http://127.0.0.1:8098`.
## Topology
| Component | Listen | Unit / mechanism |
|-----------|--------|------------------|
| API (`FundLab.Api.dll`, Release publish) | `127.0.0.1:5080` | `fundlab-api.service` |
| Web gateway (nginx: static `dist/` + `/api`,`/health` proxy) | `127.0.0.1:5176` (frontend) and `127.0.0.1:8098` (tunnel origin) | `fundlab-gateway.service` |
| PostgreSQL 16 (real data dir, docker named volume) | `127.0.0.1:55435` | docker container `fundlab-pg` (`--restart unless-stopped`) |
Public request path: `cloudflared` -> `127.0.0.1:8098` (gateway) -> static
`src/FundLab.Web/dist` for `/`, and reverse-proxy to the API for `/api/*` and
`/health`.
### Port note (necessary deviation)
The tunnel origin is `8098`, but a single origin must serve **both** the
frontend (`/`) and the API (`/api`, `/health`). The API therefore listens on
`127.0.0.1:5080` and the nginx gateway owns `8098` (and `5176`). `curl
http://127.0.0.1:8098/health` still returns the API health payload because the
gateway proxies it.
## Files
- `fundlab-api.service`, `fundlab-gateway.service` — systemd user units.
- `nginx-fundlab.conf` — gateway config (high ports, unprivileged user; temp
dirs under `var/fund-lab/`).
- Runtime env/secrets: `.env.deploy` at the repo root (git-ignored via
`.env.*`; contains `FUND_LAB_DATABASE_URL`, `FUND_LAB_AUTH_TOKEN`,
`ASPNETCORE_URLS`, `FUND_LAB_AKSHARE_PYTHON`). Never commit it.
- Logs: `var/fund-lab/{api,gateway,nginx-*.log}` (git-ignored via `var/`).
## Build
```sh
# API (Release; FS3511 fixed, so the default TreatWarningsAsErrors=true applies).
# scripts/publish-check.sh guards this path (and qa/run.sh calls it).
dotnet publish src/FundLab.Api/FundLab.Api.fsproj -c Release -o src/FundLab.Api/publish
# Frontend -> src/FundLab.Web/dist
cd src/FundLab.Web
PATH="$PWD/../../.tools/node-v22.23.2/bin:$PATH" npm run build
```
## Install / run
```sh
systemctl --user link "$PWD/scripts/systemd/fundlab-api.service"
systemctl --user link "$PWD/scripts/systemd/fundlab-gateway.service"
systemctl --user daemon-reload
systemctl --user enable --now fundlab-api.service fundlab-gateway.service
```
`loginctl` linger is already enabled for this user, so the units start on boot.
## Verify
```sh
curl -sS http://127.0.0.1:5080/health
curl -sS http://127.0.0.1:5176/ # frontend HTML
curl -sS http://127.0.0.1:8098/health
curl -sS https://fund.somhairle.bid/health
curl -sS https://fund.somhairle.bid/ # frontend HTML (not Somhairle's Dream)
curl -sS -H "Authorization: Bearer $FUND_LAB_AUTH_TOKEN" \
https://fund.somhairle.bid/api/portfolio/summary
```
Auth is a static Bearer token (`FUND_LAB_AUTH_TOKEN`); there is no `/login`
endpoint. `/health` is anonymous; everything under `/api` requires the token.
|