docs: generalize deployment section to any reverse proxy #73
49
README.md
49
README.md
@@ -8,7 +8,7 @@ A full-stack app in one repo:
|
|||||||
|
|
||||||
- **Backend** — Node.js + Express REST API (layered `router → controller → model → db`), MariaDB, a provider-agnostic session layer (JWT cookie for web, bearer tokens for mobile, pluggable SSO).
|
- **Backend** — Node.js + Express REST API (layered `router → controller → model → db`), MariaDB, a provider-agnostic session layer (JWT cookie for web, bearer tokens for mobile, pluggable SSO).
|
||||||
- **Frontend** — React + Vite single-page app (public site, wiki, and the admin panel), dark "gothic" theme (Cinzel + Georgia).
|
- **Frontend** — React + Vite single-page app (public site, wiki, and the admin panel), dark "gothic" theme (Cinzel + Georgia).
|
||||||
- **Deploy** — Docker Compose (app + MariaDB) behind a Pangolin reverse proxy. Express serves the built SPA in production.
|
- **Deploy** — Docker Compose (app + MariaDB) behind a reverse proxy (Pangolin, Nginx, Caddy, Traefik, …). Express serves the built SPA in production.
|
||||||
- **Shard link** — a live bridge to the in-game ServUO shard through the **uo-link** sidecar ([RunicGateway/link](https://gitea.whitlocktech.com/RunicGateway/link)): the site ingests a live event feed and makes server-side REST calls to show shard status, economy, staff presence, IDOCs, live activity, and per-character sheets. See [Shard integration (uo-link)](#shard-integration-uo-link).
|
- **Shard link** — a live bridge to the in-game ServUO shard through the **uo-link** sidecar ([RunicGateway/link](https://gitea.whitlocktech.com/RunicGateway/link)): the site ingests a live event feed and makes server-side REST calls to show shard status, economy, staff presence, IDOCs, live activity, and per-character sheets. See [Shard integration (uo-link)](#shard-integration-uo-link).
|
||||||
|
|
||||||
The design reference is [BACKEND_DESIGN.md](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/BACKEND_DESIGN.md) (API contract, schema, security), in the [**RunicGateway/docs**](https://gitea.whitlocktech.com/RunicGateway/docs) repo — where all project documentation now lives.
|
The design reference is [BACKEND_DESIGN.md](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/BACKEND_DESIGN.md) (API contract, schema, security), in the [**RunicGateway/docs**](https://gitea.whitlocktech.com/RunicGateway/docs) repo — where all project documentation now lives.
|
||||||
@@ -32,7 +32,7 @@ The design reference is [BACKEND_DESIGN.md](https://gitea.whitlocktech.com/Runic
|
|||||||
- [Environment variables](#environment-variables)
|
- [Environment variables](#environment-variables)
|
||||||
- [Security](#security)
|
- [Security](#security)
|
||||||
- [Logging](#logging)
|
- [Logging](#logging)
|
||||||
- [Deployment behind Pangolin](#deployment-behind-pangolin)
|
- [Deployment behind a reverse proxy](#deployment-behind-a-reverse-proxy)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ The design reference is [BACKEND_DESIGN.md](https://gitea.whitlocktech.com/Runic
|
|||||||
| Frontend | React 18, Vite 5, React Router 6 |
|
| Frontend | React 18, Vite 5, React Router 6 |
|
||||||
| Email | Nodemailer via Gmail OAuth2 (configured in admin), with a `mailto:` fallback |
|
| Email | Nodemailer via Gmail OAuth2 (configured in admin), with a `mailto:` fallback |
|
||||||
| API docs | OpenAPI 3.0 via `swagger-autogen`, served with `swagger-ui-express` at `/api/docs` |
|
| API docs | OpenAPI 3.0 via `swagger-autogen`, served with `swagger-ui-express` at `/api/docs` |
|
||||||
| Deploy | Docker Compose, Pangolin reverse proxy |
|
| Deploy | Docker Compose, any reverse proxy (Pangolin, Nginx, Caddy, Traefik, …) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -355,7 +355,7 @@ Copy `.env.example` (Compose) or `server/.env.example` (local) and fill in. **`.
|
|||||||
| `DB_ROOT_PASSWORD` | — | MariaDB root (Compose only) |
|
| `DB_ROOT_PASSWORD` | — | MariaDB root (Compose only) |
|
||||||
| `JWT_SECRET` | — | **required** — long random string; signs session, mobile, and SSO-flow tokens |
|
| `JWT_SECRET` | — | **required** — long random string; signs session, mobile, and SSO-flow tokens |
|
||||||
| `JWT_EXPIRES_IN` | `1d` | web session token + cookie lifetime |
|
| `JWT_EXPIRES_IN` | `1d` | web session token + cookie lifetime |
|
||||||
| `COOKIE_SECURE` | `auto` | `auto` = Secure only over HTTPS (works on LAN HTTP + Pangolin HTTPS) |
|
| `COOKIE_SECURE` | `auto` | `auto` = Secure only over HTTPS (works on LAN HTTP + proxy HTTPS) |
|
||||||
| `COOKIE_NAME` | `rg_token` | changing it on a live instance invalidates existing sessions |
|
| `COOKIE_NAME` | `rg_token` | changing it on a live instance invalidates existing sessions |
|
||||||
| `BRAND_*` | Runic Gateway | instance branding (name, tagline, colors, logo/hero/favicon) — see [Branding](#branding) |
|
| `BRAND_*` | Runic Gateway | instance branding (name, tagline, colors, logo/hero/favicon) — see [Branding](#branding) |
|
||||||
| `SECRET_ENC_KEY` | — | **required in prod** — key for AES-256-GCM encryption of stored OAuth client secrets. Dev falls back to a key derived from `JWT_SECRET` (with a warning) |
|
| `SECRET_ENC_KEY` | — | **required in prod** — key for AES-256-GCM encryption of stored OAuth client secrets. Dev falls back to a key derived from `JWT_SECRET` (with a warning) |
|
||||||
@@ -460,7 +460,7 @@ run this repo as UOMysticmoon.
|
|||||||
**Platform**
|
**Platform**
|
||||||
|
|
||||||
- `helmet`, admin routes `noindex` + `robots.txt` disallow, `trust proxy` for correct client IPs
|
- `helmet`, admin routes `noindex` + `robots.txt` disallow, `trust proxy` for correct client IPs
|
||||||
behind Pangolin (see `TRUST_PROXY`), first admin seeded from env (no hardcoded credentials),
|
behind a reverse proxy (see `TRUST_PROXY`), first admin seeded from env (no hardcoded credentials),
|
||||||
`.env` git-ignored. Passwords and request bodies are never logged. Email sends through Gmail
|
`.env` git-ignored. Passwords and request bodies are never logged. Email sends through Gmail
|
||||||
OAuth2 configured in the admin (refresh token stored AES-GCM-encrypted, never in env); the
|
OAuth2 configured in the admin (refresh token stored AES-GCM-encrypted, never in env); the
|
||||||
contact form falls back to a `mailto:` link when unconfigured.
|
contact form falls back to a `mailto:` link when unconfigured.
|
||||||
@@ -487,13 +487,42 @@ bind-mounted to `./logs/app.log` and `docker compose logs -f app` shows the cons
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Deployment behind Pangolin
|
## Deployment behind a reverse proxy
|
||||||
|
|
||||||
`docker compose up -d --build` exposes the `app` container on `0.0.0.0:3000` (no `127.0.0.1`
|
`docker compose up -d --build` exposes the `app` container on `0.0.0.0:3000` (no `127.0.0.1`
|
||||||
binding) so Pangolin can reach it. Point a Pangolin resource at `app:3000`. Because `COOKIE_SECURE`
|
binding) so a reverse proxy — Pangolin, Nginx, Caddy, Traefik, etc. — can reach it. Point the
|
||||||
defaults to `auto`, the admin login works both directly via the LAN IP over HTTP **and** through
|
proxy at `app:3000` (or the host's `:3000` if the proxy runs outside Compose) and terminate TLS
|
||||||
Pangolin over HTTPS — no config change needed. MariaDB stays on the private Compose network
|
there. Because `COOKIE_SECURE` defaults to `auto`, the admin login works both directly via the
|
||||||
(no published port by default); data persists in the `dbdata` volume, uploads in `uploads`.
|
LAN IP over HTTP **and** through the proxy over HTTPS — no config change needed. MariaDB stays on
|
||||||
|
the private Compose network (no published port by default); data persists in the `dbdata` volume,
|
||||||
|
uploads in `uploads`.
|
||||||
|
|
||||||
|
Set `TRUST_PROXY` so Express reads the real client IP from the proxy's `X-Forwarded-For` header
|
||||||
|
(see [Environment variables](#environment-variables)) — required for rate limiting, bot scoring,
|
||||||
|
and correct logging. Forward the standard `X-Forwarded-For` and `X-Forwarded-Proto` headers from
|
||||||
|
your proxy.
|
||||||
|
|
||||||
|
Minimal proxy examples:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# Nginx
|
||||||
|
location / {
|
||||||
|
proxy_pass http://app:3000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```caddy
|
||||||
|
# Caddy — Caddyfile (automatic HTTPS; forwards X-Forwarded-* by default)
|
||||||
|
your.domain {
|
||||||
|
reverse_proxy app:3000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pangolin:** create a resource targeting `app:3000`; it forwards the required headers and
|
||||||
|
terminates HTTPS out of the box, so no extra configuration is needed.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user