- Replace gorilla/csrf with net/http CrossOriginProtection - Require valkey-password and add TLS options for session store - End session at /logout and revoke refresh tokens - Re-derive identity and roles from provider every five minutes - Process each Stripe webhook event in its own Temporal workflow - Give each outbox entry its own workflow with Temporal retries - Guard against stale Stripe events with provider timestamps - Derive transport security from base-url scheme
205 lines
11 KiB
Markdown
205 lines
11 KiB
Markdown
---
|
|
title: "Production Deployment"
|
|
audience: [admin]
|
|
summary: "The sequenced path from a built image to a running production instance: what order to configure things in, and the glue between the per-service guides that no single one of them covers."
|
|
---
|
|
|
|
# Production Deployment
|
|
|
|
This guide sequences the existing per-service guides into one path and adds
|
|
the connective pieces none of them owns on their own: the reverse proxy and
|
|
trusted-origin contract, matching the identity provider's branding to the
|
|
console's configured name, the Stripe webhook flow, and what happens the
|
|
first time the process boots. It links out to each per-service guide instead
|
|
of repeating it — read this first to know what order to do things in and
|
|
where each step's real instructions live.
|
|
|
|
> **Status.** member-console is pre-production software (see the root
|
|
> [README](../README.md)). This guide gets a self-hoster to a working
|
|
> instance; it does not claim a hardened, one-command production rollout.
|
|
> Expect to adapt each step to your own infrastructure.
|
|
|
|
## 1. Build the image and prepare secrets
|
|
|
|
Follow [hosting.md](hosting.md) to build the container image for your
|
|
registry and generate the `csrf-secret`. Every other secret this instance
|
|
needs — the OIDC client secret, the Stripe API key and webhook secret, the
|
|
FedWiki admin token, the Discourse API key — is generated or obtained in the
|
|
later steps below and injected the same way: either as an `MC_*` environment
|
|
variable or as a file referenced by that key's `-file` variant (`MC_*_FILE`),
|
|
per [environment-reference.md](environment-reference.md). Prefer the `-file`
|
|
form with your platform's secret-mounting mechanism (Docker/Podman secrets,
|
|
a Kubernetes `Secret` volume) over putting secret values directly in the
|
|
environment.
|
|
|
|
## 2. Decide the deployment shape
|
|
|
|
Read [deployment-architecture.md](deployment-architecture.md) for how
|
|
member-console fits alongside a public homepage and an identity provider —
|
|
it is infrastructure behind your own front door, not the front door itself.
|
|
Decide now which services member-console needs for your offering:
|
|
|
|
- **PostgreSQL** and **Valkey** — always required.
|
|
- **An OIDC identity provider** — always required (any OIDC-compliant
|
|
provider; see step 4).
|
|
- **Temporal** — required for provisioning, integration sync, and billing
|
|
sweeps to actually run; the console starts without it but logs
|
|
`Temporal not configured` and every scheduled job stays off.
|
|
- **A FedWiki farm** — required only if you offer FedWiki sites, the
|
|
product's first conforming integration; see step 6.
|
|
- **Stripe** — required only for paid products; a deployment offering only
|
|
free/unmetered products can skip it entirely.
|
|
- **Discourse** — optional, for forum-membership sync.
|
|
|
|
The test stack's `test/compose.yaml` is a useful reference for how these
|
|
services relate to each other and to member-console (ports, environment
|
|
wiring, health-check ordering) — read it as a **shape example**, not a
|
|
supported production compose file. It runs Keycloak, not-yet-hardened
|
|
secrets, and `sslmode=disable`, all of which are deliberately wrong for
|
|
production.
|
|
|
|
## 3. Reverse proxy, TLS, and trusted origins
|
|
|
|
member-console does not terminate TLS itself; put a reverse proxy in front
|
|
of it (Caddy, nginx, Traefik, your platform's ingress) that:
|
|
|
|
- Terminates TLS and forwards plain HTTP to the console.
|
|
- Forwards the `Host` header unchanged, or otherwise ensures the console
|
|
sees a request whose host matches `base-url`'s host exactly.
|
|
- Passes the `Sec-Fetch-Site` and `Origin` headers through unmodified; the
|
|
cross-origin check below reads them.
|
|
|
|
Set `base-url` (`MC_BASE_URL`) to the console's public URL, e.g.
|
|
`https://console.example.com`. Three things follow from it at boot:
|
|
|
|
- Its origin, scheme included, is the console's **sole trusted origin** for
|
|
state-changing requests (`net/http.CrossOriginProtection`, wired in
|
|
`internal/server/server.go`). A request whose `Sec-Fetch-Site` says
|
|
cross-site, or whose `Origin` does not match, is rejected. There is no
|
|
separate trusted-origins list, so a reverse proxy that fronts the console at
|
|
a second public hostname breaks form posts from that hostname.
|
|
- Its scheme decides the session cookie's `Secure` flag: `https` sets it, so
|
|
browsers refuse to send the cookie over a plain-HTTP connection. This is
|
|
why TLS at the proxy is not optional, and why `base-url` must say `https`
|
|
when the proxy serves it: a plain-http `base-url` behind a TLS proxy ships
|
|
the cookie without `Secure`.
|
|
- The same scheme turns on the CSP `upgrade-insecure-requests` directive and
|
|
the `Strict-Transport-Security` header (`max-age=31536000;
|
|
includeSubDomains`). HSTS is a commitment: a browser that has seen it will
|
|
refuse plain HTTP for the console's hostname, and will treat any
|
|
certificate error there as fatal with no click-through, for a year after
|
|
its last visit. Keep certificate renewal automatic. `includeSubDomains`
|
|
reaches subdomains of the console's host only, which is nothing else at
|
|
`console.example.com`; at an apex hostname it would reach every subdomain
|
|
you have. If the host must ever go back to plain HTTP, serve
|
|
`Strict-Transport-Security: max-age=0` over https first, from the proxy or
|
|
by hand, until visitors have picked it up.
|
|
|
|
`env` (`MC_ENV`) no longer affects any of this; it selects the log format
|
|
only (`development` for text, anything else for JSON).
|
|
|
|
## 4. Identity provider
|
|
|
|
Follow [identity-provider-setup.md](identity-provider-setup.md) to configure
|
|
your OIDC provider: the client, the claim shape, and the `operator-member`
|
|
role that gates the operator panel.
|
|
|
|
**Match the realm's display branding to `deployment-name`.** The sign-in
|
|
page a member sees is rendered by the identity provider, not by
|
|
member-console — the console only ever redirects to it and receives a token
|
|
back. If the console is configured with `deployment-name` (`MC_DEPLOYMENT_NAME`,
|
|
see [`internal/config/deployment.go`](../internal/config/deployment.go)) set
|
|
to your organization's name, but the IdP's realm still shows Keycloak's
|
|
default branding or a different name, a member sees two different
|
|
identities across one login flow. Set the realm's display name (Keycloak:
|
|
**Realm Settings → General → Display name** / **HTML Display name**, and
|
|
its logo/theme if you use one) to the same name as `deployment-name` so the
|
|
sign-in page and the app introduce themselves consistently.
|
|
|
|
## 5. Database
|
|
|
|
Follow [database-management.md](database-management.md) for the schema
|
|
layout and migration conventions. There is nothing extra to run before
|
|
first boot: **migrations run automatically at startup** (`member-console
|
|
start` calls `db.ConnectAndMigrate` before serving any request —
|
|
`cmd/start.go`). A fresh database becomes fully migrated on the first
|
|
successful boot; there is no separate "run migrations" step required for a
|
|
new deployment, though `member-console migrate up|down|status` is available
|
|
for out-of-band migration control (e.g. running the pending migrations before
|
|
a rolling restart).
|
|
|
|
**Check configuration before restarting.** `member-console config validate`
|
|
connects to the database, parses every stored override and the resolved
|
|
environment against each key's declared type, and exits 1 naming the first
|
|
failure and its remediation (or 0 with one line when everything parses); it
|
|
never runs migrations and never starts the server. Run it before an
|
|
upgrade's restart so a bad value (an environment variable edited by hand, or
|
|
an override saved before this check existed) surfaces in your terminal
|
|
instead of the service manager's restart loop.
|
|
|
|
**Manage stored overrides from the terminal.** `member-console config`
|
|
offers the operator panel's three override actions from the command line:
|
|
`config list [integration]` prints each declared key's environment or
|
|
default value, its stored override, and which one wins at the next boot,
|
|
secrets masked; `config set <key> <value>` parses a value the same way
|
|
the panel does and refuses it with the same sentence on a bad value;
|
|
`config clear <key>` deletes a stored override, including one whose key
|
|
no installed integration declares. All three reach the same rows the panel
|
|
does; a write applies once the app restarts.
|
|
|
|
**`member-console seed-demo` is not for production.** It inserts a fixed,
|
|
idempotent reference catalog (demo products, a plan ladder, sample
|
|
grants) so an operator panel has something to click through. Never run it
|
|
against a production database.
|
|
|
|
## 6. Integrations
|
|
|
|
Configure whichever of these your deployment actually offers. Each is
|
|
independently optional; an unconfigured one simply stays dormant (no
|
|
routes, no scheduled jobs), it does not block boot.
|
|
|
|
- **FedWiki** — [fedwiki-setup.md](fedwiki-setup.md) covers the FarmManager
|
|
security stack, the farm-side setup, and the `fedwiki-*` keys.
|
|
- **Stripe** — see the webhook flow below and [stripe.md](stripe.md) for the
|
|
full configuration, subscribed events, and catalog-to-Stripe sync.
|
|
- **Discourse** — [discourse-setup.md](discourse-setup.md) covers forum-side
|
|
prerequisites and linkage-mode selection.
|
|
- **Temporal authorization** — if you run Temporal behind its own JWT
|
|
authorizer (recommended), see
|
|
[temporal-authorization-setup.md](temporal-authorization-setup.md) for the
|
|
IdP-side claim configuration.
|
|
|
|
Every key any installed integration reads is documented in
|
|
[environment-reference.md](environment-reference.md), including which keys
|
|
are required together and which default values apply when unset.
|
|
|
|
### Stripe webhook endpoint and signing secret
|
|
|
|
Once `stripe-api-key` is set, register a webhook endpoint in the Stripe
|
|
Dashboard pointing at `https://<base-url>/webhooks/stripe` and subscribe it
|
|
to the event types listed in [stripe.md](stripe.md#webhook-endpoint). Stripe
|
|
generates a signing secret (`whsec_...`) for that endpoint when you create
|
|
it; set that value as `stripe-webhook-secret` (or point
|
|
`stripe-webhook-secret-file` at it). `stripe-api-key` and
|
|
`stripe-webhook-secret` are a required pair — the console rejects a boot
|
|
where only one is set, and treats both left empty as billing intentionally
|
|
disabled.
|
|
|
|
## 7. First boot
|
|
|
|
With the database reachable, `base-url` set, the session store's password
|
|
configured, and the OIDC keys pointed at your realm, start the process (`member-console
|
|
start`, or your image's equivalent entrypoint). At startup it, in order:
|
|
runs pending migrations, validates the aggregated configuration (failing
|
|
fast with every problem named at once if something required is missing —
|
|
see [environment-reference.md](environment-reference.md) for what's
|
|
required), connects to Temporal if configured, and starts serving.
|
|
|
|
Sign in as an operator (the account holding the `operator-member` role you
|
|
configured in step 4) and go to `/operator/setup`. It is a checklist of
|
|
everything the instance still needs before it has a deliverable product:
|
|
entitlement sets, a published product, plan ladders, and — if you configured
|
|
Stripe — a resolved billing configuration. Work through it in order; each
|
|
step links to the operator page that completes it and reflects as done once
|
|
the underlying data exists.
|