summaryrefslogtreecommitdiff
path: root/scripts/systemd
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-22 07:30:09 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-22 07:30:09 +0800
commit3d95b9c736a4cf59cf26bd9e5c7c3162ee8c0fd7 (patch)
tree843b04ddb0c3307f05cd1b5d26d4d70aec2ff0dc /scripts/systemd
parent6ba727422137b35c00b9bb983e574d23a4d15e24 (diff)
downloadfund-lab-3d95b9c736a4cf59cf26bd9e5c7c3162ee8c0fd7.tar.gz
Add fund-lab production systemd --user units and gateway config (3d-28)
Diffstat (limited to 'scripts/systemd')
-rw-r--r--scripts/systemd/README.md72
-rw-r--r--scripts/systemd/fundlab-api.service18
-rw-r--r--scripts/systemd/fundlab-gateway.service18
-rw-r--r--scripts/systemd/nginx-fundlab.conf70
4 files changed, 178 insertions, 0 deletions
diff --git a/scripts/systemd/README.md b/scripts/systemd/README.md
new file mode 100644
index 0000000..7e3fdaa
--- /dev/null
+++ b/scripts/systemd/README.md
@@ -0,0 +1,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 warning suppressed only for publish)
+dotnet publish src/FundLab.Api/FundLab.Api.fsproj -c Release \
+ -p:TreatWarningsAsErrors=false -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.
diff --git a/scripts/systemd/fundlab-api.service b/scripts/systemd/fundlab-api.service
new file mode 100644
index 0000000..ecdbd9b
--- /dev/null
+++ b/scripts/systemd/fundlab-api.service
@@ -0,0 +1,18 @@
+[Unit]
+Description=fund-lab API (loopback 5080, real Postgres + real AKShare interpreter)
+Documentation=file:/home/somhairle/projects/fund-lab/scripts/systemd/README.md
+After=network-online.target
+Wants=network-online.target
+
+[Service]
+Type=simple
+WorkingDirectory=/home/somhairle/projects/fund-lab
+EnvironmentFile=/home/somhairle/projects/fund-lab/.env.deploy
+ExecStart=/usr/bin/dotnet /home/somhairle/projects/fund-lab/src/FundLab.Api/publish/FundLab.Api.dll
+Restart=on-failure
+RestartSec=3
+StandardOutput=append:/home/somhairle/projects/fund-lab/var/fund-lab/api.log
+StandardError=append:/home/somhairle/projects/fund-lab/var/fund-lab/api.log
+
+[Install]
+WantedBy=default.target
diff --git a/scripts/systemd/fundlab-gateway.service b/scripts/systemd/fundlab-gateway.service
new file mode 100644
index 0000000..24500ef
--- /dev/null
+++ b/scripts/systemd/fundlab-gateway.service
@@ -0,0 +1,18 @@
+[Unit]
+Description=fund-lab web gateway (loopback 5176 frontend + 8098 tunnel origin -> static dist + /api,/health proxy)
+Documentation=file:/home/somhairle/projects/fund-lab/scripts/systemd/README.md
+After=network-online.target fundlab-api.service
+Wants=network-online.target
+
+[Service]
+Type=simple
+WorkingDirectory=/home/somhairle/projects/fund-lab
+ExecStartPre=/bin/mkdir -p /home/somhairle/projects/fund-lab/var/fund-lab/nginx-tmp
+ExecStart=/usr/sbin/nginx -c /home/somhairle/projects/fund-lab/scripts/systemd/nginx-fundlab.conf -g 'daemon off;'
+Restart=on-failure
+RestartSec=3
+StandardOutput=append:/home/somhairle/projects/fund-lab/var/fund-lab/gateway.log
+StandardError=append:/home/somhairle/projects/fund-lab/var/fund-lab/gateway.log
+
+[Install]
+WantedBy=default.target
diff --git a/scripts/systemd/nginx-fundlab.conf b/scripts/systemd/nginx-fundlab.conf
new file mode 100644
index 0000000..e6fdcd4
--- /dev/null
+++ b/scripts/systemd/nginx-fundlab.conf
@@ -0,0 +1,70 @@
+# fund-lab production gateway (run as the unprivileged user; high ports only).
+# Serves the built frontend from src/FundLab.Web/dist on both the frontend port
+# (5176) and the cloudflared tunnel origin port (8098), and reverse-proxies
+# /api and /health to the fund-lab API on 127.0.0.1:5080.
+
+worker_processes 1;
+pid /home/somhairle/projects/fund-lab/var/fund-lab/nginx.pid;
+error_log /home/somhairle/projects/fund-lab/var/fund-lab/nginx-error.log warn;
+
+events {
+ worker_connections 256;
+}
+
+http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+ access_log /home/somhairle/projects/fund-lab/var/fund-lab/nginx-access.log;
+
+ client_body_temp_path /home/somhairle/projects/fund-lab/var/fund-lab/nginx-tmp/client;
+ proxy_temp_path /home/somhairle/projects/fund-lab/var/fund-lab/nginx-tmp/proxy;
+ fastcgi_temp_path /home/somhairle/projects/fund-lab/var/fund-lab/nginx-tmp/fastcgi;
+ uwsgi_temp_path /home/somhairle/projects/fund-lab/var/fund-lab/nginx-tmp/uwsgi;
+ scgi_temp_path /home/somhairle/projects/fund-lab/var/fund-lab/nginx-tmp/scgi;
+
+ upstream fundlab_api {
+ server 127.0.0.1:5080;
+ }
+
+ server {
+ listen 127.0.0.1:5176;
+ server_name _;
+ root /home/somhairle/projects/fund-lab/src/FundLab.Web/dist;
+ index index.html;
+
+ location = /health {
+ proxy_pass http://fundlab_api;
+ proxy_set_header Host $host;
+ }
+
+ location /api/ {
+ proxy_pass http://fundlab_api;
+ proxy_set_header Host $host;
+ }
+
+ location / {
+ try_files $uri /index.html;
+ }
+ }
+
+ server {
+ listen 127.0.0.1:8098;
+ server_name _;
+ root /home/somhairle/projects/fund-lab/src/FundLab.Web/dist;
+ index index.html;
+
+ location = /health {
+ proxy_pass http://fundlab_api;
+ proxy_set_header Host $host;
+ }
+
+ location /api/ {
+ proxy_pass http://fundlab_api;
+ proxy_set_header Host $host;
+ }
+
+ location / {
+ try_files $uri /index.html;
+ }
+ }
+}