Doc updates

This commit is contained in:
adilallo
2026-05-23 14:55:19 -06:00
parent a14cae744d
commit a3119d3f90
3 changed files with 165 additions and 53 deletions
+21 -14
View File
@@ -668,9 +668,9 @@ All six are titled `[Backend] …`, assigned to Vinod, in the **community-rule**
| # | Linear | Title | Depends on | | # | Linear | Title | Depends on |
| - | ------ | ----- | ---------- | | - | ------ | ----- | ---------- |
| 1 | [CR-96](https://linear.app/community-rule/issue/CR-96/backend-bridge-cloudron-env-vars-to-canonical-names) | `[Backend] Bridge CLOUDRON_* env vars to canonical names` | none — can ship now | | 1 | [CR-96](https://linear.app/community-rule/issue/CR-96/backend-bridge-cloudron-env-vars-to-canonical-names) | `[Backend] Cloudron-native env vars` | **Done** — merged |
| 2 | [CR-97](https://linear.app/community-rule/issue/CR-97/backend-container-image-registry-choose-build-push) | `[Backend] Container image registry: choose, build, push` | registry decision (handoff §5) | | 2 | [CR-97](https://linear.app/community-rule/issue/CR-97/backend-container-image-registry-choose-build-push) | `[Backend] Container image registry: choose, build, push` | **Done** — first image `0.1.0` verified |
| 3 | [CR-98](https://linear.app/community-rule/issue/CR-98/backend-cloudron-staging-install-smoke) | `[Backend] Cloudron staging install + smoke` | CR-96 + CR-97 + Cloudron CLI access + staging DNS | | 3 | [CR-98](https://linear.app/community-rule/issue/CR-98/backend-cloudron-staging-install-smoke) | `[Backend] Cloudron staging install + smoke` | Cloudron CLI token (§2) — **next** |
| 4 | [CR-99](https://linear.app/community-rule/issue/CR-99/backend-cloudron-production-install-apex-cutover) | `[Backend] Cloudron production install + apex cutover` | CR-98 green for the agreed overlap window | | 4 | [CR-99](https://linear.app/community-rule/issue/CR-99/backend-cloudron-production-install-apex-cutover) | `[Backend] Cloudron production install + apex cutover` | CR-98 green for the agreed overlap window |
| 5 | [CR-100](https://linear.app/community-rule/issue/CR-100/backend-steady-state-operator-runbook) | `[Backend] Steady-state operator runbook` | CR-98 (write what we actually did) | | 5 | [CR-100](https://linear.app/community-rule/issue/CR-100/backend-steady-state-operator-runbook) | `[Backend] Steady-state operator runbook` | CR-98 (write what we actually did) |
| 6 | [CR-101](https://linear.app/community-rule/issue/CR-101/backend-decommission-legacy-communityrule-lamp-app) | `[Backend] Decommission legacy CommunityRule LAMP app` | CR-99 + sign-off window | | 6 | [CR-101](https://linear.app/community-rule/issue/CR-101/backend-decommission-legacy-communityrule-lamp-app) | `[Backend] Decommission legacy CommunityRule LAMP app` | CR-99 + sign-off window |
@@ -682,21 +682,28 @@ All six are titled `[Backend] …`, assigned to Vinod, in the **community-rule**
| Order | Linear | Repo PR / branch | Kind | Status | Blocked by | | Order | Linear | Repo PR / branch | Kind | Status | Blocked by |
| ----- | ------ | ---------------- | ---- | ------ | ---------- | | ----- | ------ | ---------------- | ---- | ------ | ---------- |
| 1 | [CR-96](https://linear.app/community-rule/issue/CR-96/backend-bridge-cloudron-env-vars-to-canonical-names) | `adilallo/Backend/BridgeCloudronEnv`*[Backend] Cloudron-native environment variables* | repo | **Open** | — | | 1 | [CR-96](https://linear.app/community-rule/issue/CR-96/backend-bridge-cloudron-env-vars-to-canonical-names) | `adilallo/Backend/BridgeCloudronEnv`*[Backend] Cloudron-native environment variables* | repo | **Done** | — |
| 2 | [CR-97](https://linear.app/community-rule/issue/CR-97/backend-container-image-registry-choose-build-push) | TBD — registry choice + build/push (Dockerfile / CI) | repo | **Next** | CR-96 merged + registry decision ([ops-backend-deploy.md](ops-backend-deploy.md) §6) | | 2 | [CR-97](https://linear.app/community-rule/issue/CR-97/backend-container-image-registry-choose-build-push) | Container registry packaging + `docker-release.sh` | repo | **Done** | |
| — | [CR-102](https://linear.app/community-rule/issue/CR-102/backend-decide-fate-of-legacy-rules-table-read-only-export) | TBD — optional repo PR if export tooling/docs needed | product / repo | **Parallel** | row count from legacy MySQL (preCR-99 backup) | | — | [CR-102](https://linear.app/community-rule/issue/CR-102/backend-decide-fate-of-legacy-rules-table-read-only-export) | TBD — optional repo PR if export tooling/docs needed | product / repo | **Parallel** | row count from legacy MySQL (preCR-99 backup) |
| 3 | [CR-98](https://linear.app/community-rule/issue/CR-98/backend-cloudron-staging-install-smoke) | — (ops checklist; doc tweaks only if smoke finds gaps) | ops | Backlog | CR-96 + CR-97 + Cloudron CLI token + staging DNS | | 3 | [CR-98](https://linear.app/community-rule/issue/CR-98/backend-cloudron-staging-install-smoke) | — (ops checklist; [ops-backend-deploy.md §10](ops-backend-deploy.md#10-staging-install--smoke-cr-98)) | ops | **Next** | Cloudron CLI token only |
| 4 | [CR-100](https://linear.app/community-rule/issue/CR-100/backend-steady-state-operator-runbook) | TBD — `docs/guides/ops-runbook.md` | docs | Backlog | CR-98 (write what we actually did) | | 4 | [CR-100](https://linear.app/community-rule/issue/CR-100/backend-steady-state-operator-runbook) | TBD — `docs/guides/ops-runbook.md` | docs | Backlog | CR-98 (write what we actually did) |
| 5 | [CR-99](https://linear.app/community-rule/issue/CR-99/backend-cloudron-production-install-apex-cutover) | — (ops; maintenance window) | ops | Backlog | CR-98 green + CR-102 resolved | | 5 | [CR-99](https://linear.app/community-rule/issue/CR-99/backend-cloudron-production-install-apex-cutover) | — (ops; maintenance window) | ops | Backlog | CR-98 green + CR-102 resolved |
| 6 | [CR-101](https://linear.app/community-rule/issue/CR-101/backend-decommission-legacy-communityrule-lamp-app) | — (ops; uninstall LAMP slot) | ops | Backlog | CR-99 + sign-off window | | 6 | [CR-101](https://linear.app/community-rule/issue/CR-101/backend-decommission-legacy-communityrule-lamp-app) | — (ops; uninstall LAMP slot) | ops | Backlog | CR-99 + sign-off window |
**What's next:** merge **CR-96** PR, then open **CR-97** on its own branch. Start **CR-102** product decision in parallel so it is resolved before the CR-99 cutover window. **What's next:** **CR-98** — staging install + smoke at `staging.communityrule.info`
([ops-backend-deploy.md §10](ops-backend-deploy.md#10-staging-install--smoke-cr-98)).
Start **CR-102** product decision in parallel so it is resolved before the CR-99
cutover window.
**Per-ticket detail:** **Per-ticket detail:**
1. **Cloudron-native env vars (CR-96).** **Shipped in repo** on branch `adilallo/Backend/BridgeCloudronEnv` (PR open). App reads `CLOUDRON_POSTGRESQL_URL` and `CLOUDRON_MAIL_SMTP_*` only (no `DATABASE_URL` / `SMTP_URL` shim). Local dev uses the same names in `.env`. SMTP URL assembled in [`lib/server/env.ts`](../../lib/server/env.ts); mail senders use `getSmtpUrl()`. Acceptance: with only `CLOUDRON_*` set, app connects to DB and sends mail; unit tests in `tests/unit/env.test.ts`. 1. **Cloudron-native env vars (CR-96).** **Done.** App reads `CLOUDRON_POSTGRESQL_URL` and `CLOUDRON_MAIL_SMTP_*` only (no `DATABASE_URL` / `SMTP_URL` shim). Prisma datasource uses `CLOUDRON_POSTGRESQL_URL`. Local dev uses the same names in `.env`. SMTP URL assembled in [`lib/server/env.ts`](../../lib/server/env.ts); mail senders use `getSmtpUrl()`. `scripts/start.sh` does not bridge env names.
2. **Container image registry: choose, build, push.** Acceptance: `docker pull <registry>/communityrule:<tag>` works from a Cloudron-reachable network. CI builds and pushes on merge to `main` (stretch). 2. **Container image registry (CR-97).** **Done.** Gitea registry on `git.medlab.host`; repo `CommunityRule/community-rule` is **public** (package visibility inherits from repo). Image `git.medlab.host/communityrule/community-rule:0.1.0`, built `linux/amd64` via `./scripts/docker-release.sh`. Anonymous pull verified. CI on merge to `main` deferred (no hosted runners).
3. **Cloudron staging install + smoke.** Acceptance: `curl https://<staging>/api/health` returns `{"ok":true,"database":"connected"}`; magic-link request → click link → `GET /api/auth/session` returns a user; publishing a rule succeeds. 3. **Cloudron staging install + smoke (CR-98).** **Next.** Full checklist in [ops-backend-deploy.md §10](ops-backend-deploy.md#10-staging-install--smoke-cr-98). Summary:
- **Prereqs:** Cloudron CLI token (only outstanding item); CR-96 + CR-97 done.
- **Install:** `cloudron install --location staging.communityrule.info` from repo root (manifest supplies `dockerimage`; migrations run in `start.sh`).
- **Configure:** `SESSION_SECRET`, `SMTP_FROM`, `NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true`, `UPLOAD_ROOT=/app/data/uploads`.
- **Acceptance:** `GET /api/health``{"ok":true,"database":"connected"}`; magic-link sign-in end-to-end; publish a rule succeeds.
4. **Cloudron production install + DNS cutover.** Acceptance: production subdomain resolves to the new app; old subdomain still works during overlap; sign-in + publish succeed against production; backups confirmed. 4. **Cloudron production install + DNS cutover.** Acceptance: production subdomain resolves to the new app; old subdomain still works during overlap; sign-in + publish succeed against production; backups confirmed.
5. **Steady-state operator runbook.** Lives at `docs/guides/ops-runbook.md` (sibling to the handoff). Covers deploy a new version, rollback, restore drill cadence, multi-instance limitations from [`backend-roadmap.md`](backend-roadmap.md) §5/§7. Acceptance: a fresh reader can deploy + roll back using only this doc. 5. **Steady-state operator runbook.** Lives at `docs/guides/ops-runbook.md` (sibling to the handoff). Covers deploy a new version, rollback, restore drill cadence, multi-instance limitations from [`backend-roadmap.md`](backend-roadmap.md) §5/§7. Acceptance: a fresh reader can deploy + roll back using only this doc.
6. **Decommission legacy Express/MySQL backend.** Acceptance: old Cloudron app stopped + uninstalled; old MySQL addon backed up once and removed; legacy Gitea repo README updated to point at this app. Priority: Low. 6. **Decommission legacy Express/MySQL backend.** Acceptance: old Cloudron app stopped + uninstalled; old MySQL addon backed up once and removed; legacy Gitea repo README updated to point at this app. Priority: Low.
@@ -822,7 +829,7 @@ Tickets **1011** can be deferred without blocking the core “auth + drafts +
## Linear (Community-rule team) ## Linear (Community-rule team)
**Main chain (historical):** **CR-72 → CR-83** was the original **strict sequence**; **repo + Linear status today:** **CR-72CR-79**, **CR-83**, **CR-84**, **CR-85**, **CR-88**, **CR-89** are **Done**; **CR-77** (publish) **Done**; **CR-80CR-81** remain **Backlog** (web vitals, public rule detail). **CR-82** covered by local `migrate:smoke` (see Ticket 11). **CR-83** (admin handoff) shipped as a narrow handoff sheet; the Cloudron deployment pipeline is split into **`[Backend]` follow-ups CR-96CR-102** — see [PR plan (CR-96 CR-102)](#pr-plan-cr-96--cr-102) under Ticket 12 (**CR-96** repo PR open; **CR-97** next). **Parallel (still open):** **CR-86** / Ticket 15 (**Backlog** — publish **not** a blocker); **CR-103** / Ticket 20 (change account email); **CR-93** (**Backlog**); **CR-90** / Ticket 18 (stakeholder invites); **CR-91** / Ticket 19 (`Add` button behavior); **Ticket 21 / [CR-113](https://linear.app/community-rule/issue/CR-113/backend-create-flow-file-uploads-community-photo-custom-method)** (create-flow file uploads). **Main chain (historical):** **CR-72 → CR-83** was the original **strict sequence**; **repo + Linear status today:** **CR-72CR-79**, **CR-83**, **CR-84**, **CR-85**, **CR-88**, **CR-89** are **Done**; **CR-77** (publish) **Done**; **CR-80CR-81** remain **Backlog** (web vitals, public rule detail). **CR-82** covered by local `migrate:smoke` (see Ticket 11). **CR-83** (admin handoff) shipped as a narrow handoff sheet; the Cloudron deployment pipeline is split into **`[Backend]` follow-ups CR-96CR-102** — see [PR plan (CR-96 CR-102)](#pr-plan-cr-96--cr-102) under Ticket 12 (**CR-96** + **CR-97** **Done**; **CR-98** **next**). **Parallel (still open):** **CR-86** / Ticket 15 (**Backlog** — publish **not** a blocker); **CR-103** / Ticket 20 (change account email); **CR-93** (**Backlog**); **CR-90** / Ticket 18 (stakeholder invites); **CR-91** / Ticket 19 (`Add` button behavior); **Ticket 21 / [CR-113](https://linear.app/community-rule/issue/CR-113/backend-create-flow-file-uploads-community-photo-custom-method)** (create-flow file uploads).
| Doc ticket | Linear | Title (short) | Deploy PR / tracking | | Doc ticket | Linear | Title (short) | Deploy PR / tracking |
| ---------: | --------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | -------------------- | | ---------: | --------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | -------------------- |
@@ -838,9 +845,9 @@ Tickets **1011** can be deferred without blocking the core “auth + drafts +
| 10 | [CR-81](https://linear.app/community-rule/issue/CR-81/backend-public-rule-detail-page-get-apirulesid-optional) | Public rule detail (optional) | — | | 10 | [CR-81](https://linear.app/community-rule/issue/CR-81/backend-public-rule-detail-page-get-apirulesid-optional) | Public rule detail (optional) | — |
| 11 | [CR-82](https://linear.app/community-rule/issue/CR-82/backend-ci-postgres-migration-smoke-optional) | Local migrate smoke (**Done in repo**; optional remote CI) | — | | 11 | [CR-82](https://linear.app/community-rule/issue/CR-82/backend-ci-postgres-migration-smoke-optional) | Local migrate smoke (**Done in repo**; optional remote CI) | — |
| 12 | [CR-83](https://linear.app/community-rule/issue/CR-83/backend-stagingproduction-runbook-admin-handoff-docsops-backend) | Ops admin handoff (Cloudron) **Done** | — | | 12 | [CR-83](https://linear.app/community-rule/issue/CR-83/backend-stagingproduction-runbook-admin-handoff-docsops-backend) | Ops admin handoff (Cloudron) **Done** | — |
| 12.1 | [CR-96](https://linear.app/community-rule/issue/CR-96/backend-bridge-cloudron-env-vars-to-canonical-names) | Cloudron-native env vars | **Open**`adilallo/Backend/BridgeCloudronEnv` | | 12.1 | [CR-96](https://linear.app/community-rule/issue/CR-96/backend-bridge-cloudron-env-vars-to-canonical-names) | Cloudron-native env vars | **Done** |
| 12.2 | [CR-97](https://linear.app/community-rule/issue/CR-97/backend-container-image-registry-choose-build-push) | Container image registry + CI | **Next**own branch after CR-96 | | 12.2 | [CR-97](https://linear.app/community-rule/issue/CR-97/backend-container-image-registry-choose-build-push) | Container image registry + CI | **Done**image `0.1.0` |
| 12.3 | [CR-98](https://linear.app/community-rule/issue/CR-98/backend-cloudron-staging-install-smoke) | Cloudron staging install + smoke | Ops — after CR-96 + CR-97 | | 12.3 | [CR-98](https://linear.app/community-rule/issue/CR-98/backend-cloudron-staging-install-smoke) | Cloudron staging install + smoke | **Next** — [ops-backend-deploy.md §10](ops-backend-deploy.md#10-staging-install--smoke-cr-98) |
| 12.4 | [CR-99](https://linear.app/community-rule/issue/CR-99/backend-cloudron-production-install-apex-cutover) | Production install + apex cutover | Ops — after CR-98 + CR-102 | | 12.4 | [CR-99](https://linear.app/community-rule/issue/CR-99/backend-cloudron-production-install-apex-cutover) | Production install + apex cutover | Ops — after CR-98 + CR-102 |
| 12.5 | [CR-100](https://linear.app/community-rule/issue/CR-100/backend-steady-state-operator-runbook) | Steady-state operator runbook | Docs PR — after CR-98 | | 12.5 | [CR-100](https://linear.app/community-rule/issue/CR-100/backend-steady-state-operator-runbook) | Steady-state operator runbook | Docs PR — after CR-98 |
| 12.6 | [CR-101](https://linear.app/community-rule/issue/CR-101/backend-decommission-legacy-communityrule-lamp-app) | Decommission legacy LAMP app | Ops — after CR-99 + sign-off | | 12.6 | [CR-101](https://linear.app/community-rule/issue/CR-101/backend-decommission-legacy-communityrule-lamp-app) | Decommission legacy LAMP app | Ops — after CR-99 + sign-off |
+140 -36
View File
@@ -3,8 +3,10 @@
This doc captures everything needed to deploy the new CommunityRule This doc captures everything needed to deploy the new CommunityRule
(Next.js + Postgres) onto MEDLab's Cloudron and replace the legacy (Next.js + Postgres) onto MEDLab's Cloudron and replace the legacy
LAMP-packaged service at `communityrule.info`. Cloudron admin access LAMP-packaged service at `communityrule.info`. Cloudron admin access
has been granted and the container registry is wired up (§6, §9); the has been granted, CR-96 (Cloudron-native env vars) and CR-97 (container
remaining gates are CR-96 (env bridging) and CR-98 (staging install). registry + first image push) are done; the remaining gate is
[CR-98](https://linear.app/community-rule/issue/CR-98/backend-cloudron-staging-install-smoke)
(staging install + smoke — §10).
> **For a plain-language summary to hand to MEDLab's Cloudron admin, > **For a plain-language summary to hand to MEDLab's Cloudron admin,
> see [`../relaunch-brief.md`](../relaunch-brief.md).** This doc is the > see [`../relaunch-brief.md`](../relaunch-brief.md).** This doc is the
@@ -76,10 +78,12 @@ per-app in the manifest and provisioned at install time.
with the legacy service; SES relay accepts it). with the legacy service; SES relay accepts it).
- `NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true` — turns on Postgres draft - `NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true` — turns on Postgres draft
persistence for signed-in users. Required in production. persistence for signed-in users. Required in production.
- `UPLOAD_ROOT` — absolute path to a writable directory (typically on the - `UPLOAD_ROOT` — absolute path to a writable directory on the Cloudron
Cloudron **localstorage** mount) for `POST /api/uploads` (community photo + **localstorage** mount for `POST /api/uploads` (community photo +
custom-method attachments). When unset, upload routes return custom-method attachments). Use **`/app/data/uploads`** on Cloudron
`server_misconfigured`. See [CONTRIBUTING.md](../../CONTRIBUTING.md) API table. (`start.sh` chowns `/app/data` for the `node` user). When unset, upload
routes return `server_misconfigured`. See [CONTRIBUTING.md](../../CONTRIBUTING.md)
API table.
## 4. Platform settings ## 4. Platform settings
@@ -89,9 +93,10 @@ per-app in the manifest and provisioned at install time.
([`app/api/health/route.ts`](../../app/api/health/route.ts) returns ([`app/api/health/route.ts`](../../app/api/health/route.ts) returns
`200 {"ok":true,"database":"connected"}` when healthy, `503` `200 {"ok":true,"database":"connected"}` when healthy, `503`
otherwise). otherwise).
- Memory limit: start at **512 MiB** (matches what the legacy LAMP - Memory limit: **768 MiB** in
app has been running fine on for two years); raise if Next.js [`CloudronManifest.json`](../../CloudronManifest.json) (`memoryLimit:
standalone OOMs under load. 805306368`). The legacy LAMP app ran at 512 MiB; raise further only if
Next.js standalone OOMs under load.
- Backups: Cloudron's automatic backups are already on for the host - Backups: Cloudron's automatic backups are already on for the host
(legacy app shows weekly snapshots ~451 MB each). Same default (legacy app shows weekly snapshots ~451 MB each). Same default
applies to new apps. applies to new apps.
@@ -107,10 +112,14 @@ apex.
### Phases ### Phases
1. **Staging install**`cloudron install --image <our-image> 1. **Staging install**from a checkout whose
--location staging.communityrule.info`. Set env vars from §3. Run [`CloudronManifest.json`](../../CloudronManifest.json) matches the pushed
`prisma migrate deploy`. Smoke per image tag, run `cloudron install --location staging.communityrule.info`.
[CR-98](https://linear.app/community-rule/issue/CR-98/backend-cloudron-staging-install-smoke). Cloudron reads `dockerimage` from the manifest (no `--image` flag). Set
manual env vars from §3. `prisma migrate deploy` runs automatically in
[`scripts/start.sh`](../../scripts/start.sh) on container start. Smoke per
[CR-98](https://linear.app/community-rule/issue/CR-98/backend-cloudron-staging-install-smoke)
(§12).
2. **Soft launch / acceptance** — share the staging URL with a small 2. **Soft launch / acceptance** — share the staging URL with a small
group, exercise sign-in + publish + draft sync end-to-end. Hold group, exercise sign-in + publish + draft sync end-to-end. Hold
here until confident. here until confident.
@@ -152,18 +161,24 @@ Product decisions (closed):
Infra decision closed: Infra decision closed:
3. **Container registry — Gitea Container Registry on `git.medlab.host`.** 3. **Container registry — Gitea Container Registry on `git.medlab.host`.**
Same host as Cloudron (`193.46.198.90`); container package is set Same host as Cloudron (`193.46.198.90`). The
**public** to sidestep the [same-host docker-login "socket hangup" [`CommunityRule/community-rule`](https://git.medlab.host/CommunityRule/community-rule)
repo must be **public** so the container package inherits public visibility
(Gitea does not expose per-package visibility toggles — visibility follows
the owning repo). Public pull sidesteps the [same-host docker-login
"socket hangup"
bug](https://forum.cloudron.io/topic/14572/private-docker-registry-in-cloudron), bug](https://forum.cloudron.io/topic/14572/private-docker-registry-in-cloudron),
so Cloudron pulls without credentials. Push auth from operator so Cloudron pulls without credentials. Push auth from operator laptops uses
laptops uses a Gitea personal access token (`read:package` + a Gitea personal access token (`read:package` + `write:package`). Canonical
`write:package`). Canonical image ref: image ref: `git.medlab.host/communityrule/community-rule:<tag>`. Images are
`git.medlab.host/communityrule/community-rule:<tag>`. Operator built **`linux/amd64` only** (Cloudron host is x86_64). Operator build/push
build/push workflow lives in [§9](#9-build-and-push-image-workflow). workflow lives in [§9](#9-build-and-push-image-workflow). First verified
Tracked in [CR-97](https://linear.app/community-rule/issue/CR-97/backend-container-image-registry-choose-build-push). image: `…:0.1.0` (digest
Fallback if same-host pull ever breaks: install the **Cloudron `sha256:e652f9f4bfa4154412cc9d8b63d55c94a128e8935579d101b5ab8977e2080e52`).
Container Registry** app and re-tag against its hostname; no other Tracked in [CR-97](https://linear.app/community-rule/issue/CR-97/backend-container-image-registry-choose-build-push)
changes required. (**Done**). Fallback if same-host pull ever breaks: install the **Cloudron
Container Registry** app and re-tag against its hostname; no other changes
required.
## 7. Old vs new deltas ## 7. Old vs new deltas
@@ -199,17 +214,16 @@ All filed in Linear, titled `[Backend] …`, assigned to me, in the
**Community-rule** team, **Backlog** state. **Community-rule** team, **Backlog** state.
1. [**CR-96**](https://linear.app/community-rule/issue/CR-96/backend-bridge-cloudron-env-vars-to-canonical-names) 1. [**CR-96**](https://linear.app/community-rule/issue/CR-96/backend-bridge-cloudron-env-vars-to-canonical-names)
— `[Backend] Cloudron-native env vars` (shipped: app reads `[Backend] Cloudron-native env vars` (**Done** — app reads
`CLOUDRON_POSTGRESQL_URL` and `CLOUDRON_MAIL_SMTP_*` only). `CLOUDRON_POSTGRESQL_URL` and `CLOUDRON_MAIL_SMTP_*` only).
2. [**CR-97**](https://linear.app/community-rule/issue/CR-97/backend-container-image-registry-choose-build-push) 2. [**CR-97**](https://linear.app/community-rule/issue/CR-97/backend-container-image-registry-choose-build-push)
— `[Backend] Container image registry: choose, build, push`. `[Backend] Container image registry: choose, build, push` (**Done**).
Registry decided (§6.3); packaging + build/push workflow shipped Registry decided (§6.3); packaging + build/push workflow shipped (§9).
(§9). Closes after the first verified `docker pull` of the pushed First image pushed and verified via anonymous `docker pull` (§9).
image (no Cloudron-side install required to close this ticket;
that's CR-98).
3. [**CR-98**](https://linear.app/community-rule/issue/CR-98/backend-cloudron-staging-install-smoke) 3. [**CR-98**](https://linear.app/community-rule/issue/CR-98/backend-cloudron-staging-install-smoke)
`[Backend] Cloudron staging install + smoke` at `[Backend] Cloudron staging install + smoke` at
`staging.communityrule.info`. Blocked by CR-96 + CR-97. `staging.communityrule.info`. **Next** — checklist in §10. Requires
Cloudron CLI token (§2) only; CR-96 and CR-97 are done.
4. [**CR-99**](https://linear.app/community-rule/issue/CR-99/backend-cloudron-production-install-apex-cutover) 4. [**CR-99**](https://linear.app/community-rule/issue/CR-99/backend-cloudron-production-install-apex-cutover)
`[Backend] Cloudron production install + apex cutover`. `[Backend] Cloudron production install + apex cutover`.
Side-by-side cutover at scheduled low-traffic window per §5. Side-by-side cutover at scheduled low-traffic window per §5.
@@ -273,16 +287,24 @@ standalone server.
Override the tag with `TAG=v0.1.1 ./scripts/docker-release.sh` for Override the tag with `TAG=v0.1.1 ./scripts/docker-release.sh` for
semver releases. The script prints the exact `dockerimage` line to semver releases. The script prints the exact `dockerimage` line to
paste back into the manifest. paste back into the manifest.
3. **First push only:** in Gitea, navigate to the `CommunityRule` org 3. **First push only:** confirm the
→ Packages → `community-rule` → Settings → set **Visibility: Public**. [`CommunityRule/community-rule`](https://git.medlab.host/CommunityRule/community-rule)
repo is **Public** (Settings → General). Gitea inherits container-package
visibility from the repo — there is no per-package visibility toggle. Org
owners are not required if you have repo-admin rights on this repo.
4. **Verify the pull works without credentials** (simulates Cloudron's 4. **Verify the pull works without credentials** (simulates Cloudron's
anonymous pull): anonymous pull):
```bash ```bash
docker logout git.medlab.host docker logout git.medlab.host
docker pull git.medlab.host/communityrule/community-rule:<tag> # Image is linux/amd64 only. On Apple Silicon, add --platform:
docker pull --platform linux/amd64 git.medlab.host/communityrule/community-rule:<tag>
``` ```
A bare `docker pull` on arm64 Macs fails with "no matching manifest for
linux/arm64" — that is expected and does **not** indicate an auth problem.
Cloudron (x86_64) pulls the amd64 manifest without `--platform`.
5. **Commit the manifest change** alongside any code changes that 5. **Commit the manifest change** alongside any code changes that
shipped in this build, so the manifest and image stay in lockstep. shipped in this build, so the manifest and image stay in lockstep.
@@ -311,13 +333,95 @@ today, and the manual workflow above is acceptable for v1 staging and
production. Revisit when runners return or when release cadence production. Revisit when runners return or when release cadence
justifies the runner cost. justifies the runner cost.
## 10. Rate limiting (single-instance deploys) ## 10. Staging install + smoke (CR-98)
**Goal:** Install the pushed image at `staging.communityrule.info`, configure
production env vars, and verify the vertical slice before apex cutover
([CR-99](https://linear.app/community-rule/issue/CR-99/backend-cloudron-production-install-apex-cutover)).
**Prerequisites (all satisfied unless noted):**
- [x] **CR-96** — app reads `CLOUDRON_POSTGRESQL_URL` and
`CLOUDRON_MAIL_SMTP_*` only ([`lib/server/env.ts`](../../lib/server/env.ts),
[`prisma/schema.prisma`](../../prisma/schema.prisma)). No `DATABASE_URL` /
`SMTP_URL` shim.
- [x] **CR-97** — image pushed to
`git.medlab.host/communityrule/community-rule:0.1.0` (or current tag in
manifest); repo is **public**; anonymous amd64 pull verified (§9).
- [ ] **Cloudron CLI token** — generate at *Profile → API Tokens* on
`cloud.medlab.host`; save in 1Password (§2).
- [x] **Cloudron admin login** on `cloud.medlab.host` (§2).
- [x] **DNS** — `communityrule.info` managed in Cloudron; staging subdomain
will be provisioned at install time.
**Install steps:**
1. **Checkout** a commit whose [`CloudronManifest.json`](../../CloudronManifest.json)
`version`, `dockerimage`, and `memoryLimit` match the image you intend to
run (currently `0.1.0` →
`git.medlab.host/communityrule/community-rule:0.1.0`).
2. **Log in to Cloudron CLI:**
```bash
cloudron login cloud.medlab.host
```
3. **Install** from the repo root (manifest is read automatically):
```bash
cloudron install --location staging.communityrule.info
```
Cloudron provisions **postgresql**, **sendmail**, and **localstorage**
addons from the manifest, pulls the image (no registry credentials needed),
and starts the container. `scripts/start.sh` chowns `/app/data`, runs
`prisma migrate deploy`, then execs the Next.js server.
4. **Set manual env vars** (Cloudron does not inject these):
```bash
cloudron configure --app <app-id> --set-env \
SESSION_SECRET="$(openssl rand -hex 32)" \
SMTP_FROM="Community Rule <hello@communityrule.info>" \
NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true \
UPLOAD_ROOT=/app/data/uploads
```
Rotating `SESSION_SECRET` logs everyone out. `SMTP_FROM` must be an address
SES accepts on `communityrule.info` (platform mail addon is SES-relayed with
custom-from allowed — §3).
5. **Confirm the app is running** in the Cloudron dashboard (Logs tab). Look
for a clean `prisma migrate deploy` and Next.js listening on port 3000.
**Smoke checklist (acceptance):**
- [ ] **Health:** `curl -sS https://staging.communityrule.info/api/health`
returns `200` with `{"ok":true,"database":"connected"}`.
- [ ] **Magic link:** request sign-in from the UI → email arrives at a real
inbox → click link → land signed in →
`GET /api/auth/session` returns a user. Confirm the link host matches
`staging.communityrule.info` (reverse proxy / `Host` alignment).
- [ ] **Publish:** complete create flow → publish a rule → public rule detail
loads.
- [ ] **Draft sync (optional):** signed-in Save & Exit persists to Postgres;
resume works after re-login.
- [ ] **Upload (optional):** with `UPLOAD_ROOT` set, attach a community photo
in create flow and confirm it renders after publish.
**If something fails:**
| Symptom | Likely cause | Check |
| ------- | ------------ | ----- |
| Image pull error on install | Repo still private, or wrong tag in manifest | §6.3; `docker pull --platform linux/amd64 …` from laptop |
| Health `503` / `database: disconnected` | Postgres addon not provisioned or URL missing | Cloudron app → Environment; expect `CLOUDRON_POSTGRESQL_URL` |
| Magic link not sent | Mail addon or `SMTP_FROM` | Cloudron mail logs; `CLOUDRON_MAIL_SMTP_*` vars |
| Upload `server_misconfigured` | `UPLOAD_ROOT` unset | Set to `/app/data/uploads` (§3) |
| Container crash on start | Migration failure | App logs around `prisma migrate deploy` |
**Done when:** all smoke checklist items pass. Then proceed to soft-launch
(§5 phase 2) and, when ready, [CR-99](https://linear.app/community-rule/issue/CR-99/backend-cloudron-production-install-apex-cutover)
apex cutover.
## 11. Rate limiting (single-instance deploys)
The app uses an **in-memory** rate limiter in [`lib/server/rateLimit.ts`](../../lib/server/rateLimit.ts) (magic-link requests, organizer inquiry, etc.). This is sufficient for the current **single Cloudron container** per environment. The app uses an **in-memory** rate limiter in [`lib/server/rateLimit.ts`](../../lib/server/rateLimit.ts) (magic-link requests, organizer inquiry, etc.). This is sufficient for the current **single Cloudron container** per environment.
**Before horizontal scale-out** (multiple app instances behind a load balancer), replace or back the limiter with a shared store (e.g. Redis) so per-IP / per-user windows apply across instances. Until then, document expected limits in the steady-state runbook ([CR-100](https://linear.app/community-rule/issue/CR-100/backend-steady-state-operator-runbook)). **Before horizontal scale-out** (multiple app instances behind a load balancer), replace or back the limiter with a shared store (e.g. Redis) so per-IP / per-user windows apply across instances. Until then, document expected limits in the steady-state runbook ([CR-100](https://linear.app/community-rule/issue/CR-100/backend-steady-state-operator-runbook)).
## 11. Related docs ## 12. Related docs
- [`docs/guides/backend-roadmap.md`](backend-roadmap.md) §11 - [`docs/guides/backend-roadmap.md`](backend-roadmap.md) §11
(environments) and §8 (Prisma migrations policy). (environments) and §8 (Prisma migrations policy).
+4 -3
View File
@@ -47,9 +47,10 @@ Rollback plan during the window: restore the legacy backup to a scratch Cloudron
Roughly this order: Roughly this order:
1. **Code prep**small local change so the app reads Cloudron's injected `CLOUDRON_*` env vars natively. No infra impact. 1. ~~**Code prep** — Cloudron-native env vars (CR-96).~~ **Done.**
2. **Build and push the app image** to a container registry. 2. ~~**Build and push the app image** to Gitea registry (CR-97).~~ **Done**
3. **Install at staging** subdomain, smoke test, soft launch. (`git.medlab.host/communityrule/community-rule:0.1.0`).
3. **Install at staging** subdomain, smoke test, soft launch (CR-98).
4. **Apex cutover window** — the brief downtime above. 4. **Apex cutover window** — the brief downtime above.
5. **Uninstall legacy**, archive legacy repos. 5. **Uninstall legacy**, archive legacy repos.
6. **Write the steady-state runbook** based on what actually worked. 6. **Write the steady-state runbook** based on what actually worked.