Update Docs

This commit is contained in:
adilallo
2026-05-24 16:00:11 -06:00
parent f0e193746c
commit d5d0bed88a
6 changed files with 359 additions and 34 deletions
+1
View File
@@ -30,6 +30,7 @@ These will be deleted once the backend services are stood up:
- [guides/backend-linear-tickets.md](./guides/backend-linear-tickets.md)
- [guides/template-recommendation-matrix.md](./guides/template-recommendation-matrix.md)
- [guides/ops-backend-deploy.md](./guides/ops-backend-deploy.md) — technical deploy handoff + cutover plan (Cloudron, env vars, health checks, follow-up tickets).
- [guides/ops-runbook.md](./guides/ops-runbook.md) — steady-state operator runbook: deploy, rollback, restore drill, single-instance limits.
## Cursor rules
+15 -15
View File
@@ -632,9 +632,9 @@ _Section B — Final Review screen `+` button per category:_
**Depends on:** Tickets 18 complete enough to deploy a vertical slice.
**Server / admin:** Cloudron admin access on `my.medlab.host` granted. Scope of this ticket is the **handoff doc + cutover plan** — exactly what's in place, what the side-by-side cutover looks like, and what open product/infra questions remain. The steady-state operator runbook is split out into [CR-100](https://linear.app/community-rule/issue/CR-100/backend-steady-state-operator-runbook) (we write it after we've done the work).
**Server / admin:** Cloudron admin access on `my.medlab.host` granted. Scope of this ticket is the **handoff doc + cutover plan** — exactly what's in place, what the side-by-side cutover looks like, and closed product/infra decisions. The steady-state operator runbook is [`ops-runbook.md`](ops-runbook.md) ([CR-100](https://linear.app/community-rule/issue/CR-100/backend-steady-state-operator-runbook) **Done**).
**Goal:** Short doc that captures (a) granted access + auto-injected vs. manually-set env vars + platform settings, (b) the side-by-side → apex cutover plan with the legacy `communityrule.info` service, and (c) the remaining open questions (apex vs. permanent-subdomain final URL, legacy `rules` data communication, container registry choice).
**Goal:** Short doc that captures (a) granted access + auto-injected vs. manually-set env vars + platform settings, (b) the side-by-side → apex cutover plan with the legacy `communityrule.info` service, and (c) closed product/infra decisions (final URL, legacy rules archive, container registry).
**Platform context:** Target is **Cloudron at MEDLab** (`my.medlab.host`). The legacy `communityrule.info` is a single Cloudron **LAMP** app (`lamp.cloudronapp.php74@5.1.2`, 512 MiB at apex) hosting **three things stuffed into one container** under `/app/data/public/`: the static marketing site, the Express/MySQL backend at [`CommunityRule/CommunityRuleBackend`](https://git.medlab.host/CommunityRule/CommunityRuleBackend) (kept alive by a 30-min `run.sh` watchdog on port 3000; MySQL is the LAMP package's bundled MySQL, not a Cloudron addon), and the Flask chatbot at [`CommunityRule/CommunityRuleChatBot`](https://git.medlab.host/CommunityRule/CommunityRuleChatBot) (currently crash-looping with `ModuleNotFoundError`, last touched May 2024). New app is a properly packaged Cloudron app (Docker image + `CloudronManifest.json`, **postgresql + sendmail + localstorage** addons) and replaces all three — **no data migration**. Cloudron's container supervisor replaces the watchdog.
@@ -646,7 +646,7 @@ _Section B — Final Review screen `+` button per category:_
- **§3 Env vars** split into Cloudron auto-injected (`CLOUDRON_POSTGRESQL_URL`, `CLOUDRON_MAIL_SMTP_*`) vs. manually-set (`SESSION_SECRET`, `SMTP_FROM`, `NEXT_PUBLIC_ENABLE_BACKEND_SYNC`). Notes that addons are manifest-declared, not platform-enabled, and that platform mail is SES-relayed on `communityrule.info` with custom-from allowed.
- **§4 Platform settings** (`httpPort: 3000`, `healthCheckPath: /api/health`, 512 MiB to start, automatic backups already on).
- **§5 Cutover plan** — staging at `staging.communityrule.info`, soft-launch, apex cutover at scheduled low-traffic window (~515 min downtime).
- **§6 Open questions** — apex vs. permanent subdomain final URL; legacy `rules` data communication; container registry choice.
- **§6 Decisions** — final URL (`communityrule.info` apex); legacy `rules` export to Gitea archive (§6.1); container registry (Gitea, done).
- **§7 Old vs new deltas** (LAMP-package detail, watchdog, OTP→magic link, sender, API surface, chatbot).
- **§8 Follow-up tickets** (the six tickets below).
2. Cross-links: [`docs/guides/backend-roadmap.md`](backend-roadmap.md) §11 (environments — names Cloudron at MEDLab) and §8 (migrations policy — never rewrite applied migrations).
@@ -656,7 +656,7 @@ _Section B — Final Review screen `+` button per category:_
- [x] Admin handoff covers exactly the access that was needed (most self-serve via Cloudron admin login).
- [x] Cutover plan is side-by-side and explicitly avoids in-place apex replacement.
- [x] Six follow-up tickets enumerated and linked, with CR-99 + CR-101 scope corrected to reflect that legacy is one LAMP slot containing marketing + backend + chatbot (all retire together).
- [x] Open product/infra questions surfaced rather than assumed.
- [x] Closed product/infra decisions documented (§6 + §6.1).
**Files:** [`docs/guides/ops-backend-deploy.md`](ops-backend-deploy.md), [`docs/guides/backend-roadmap.md`](backend-roadmap.md), [`docs/README.md`](../README.md), [`CONTRIBUTING.md`](../../CONTRIBUTING.md).
@@ -672,9 +672,9 @@ All six are titled `[Backend] …`, assigned to Vinod, in the **community-rule**
| 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` | 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 |
| 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` | **Done** — [ops-runbook.md](ops-runbook.md) |
| 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 |
| 7 | [CR-102](https://linear.app/community-rule/issue/CR-102/backend-decide-fate-of-legacy-rules-table-read-only-export) | `[Backend] Decide fate of legacy rules table (read-only export?)` | must resolve before CR-99 maintenance window |
| 7 | [CR-102](https://linear.app/community-rule/issue/CR-102/backend-decide-fate-of-legacy-rules-table-read-only-export) | `[Backend] Legacy rules archive export` | execute during CR-99 window (§6.1) |
### PR plan (CR-96 CR-102)
@@ -684,16 +684,16 @@ All six are titled `[Backend] …`, assigned to Vinod, in the **community-rule**
| ----- | ------ | ---------------- | ---- | ------ | ---------- |
| 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) | 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) | — (ops during CR-99; [ops-backend-deploy.md §6.1](ops-backend-deploy.md#61-legacy-rules-archive-cr-102)) | ops | **Parallel** | CR-99 window |
| 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) |
| 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 |
| 4 | [CR-100](https://linear.app/community-rule/issue/CR-100/backend-steady-state-operator-runbook) | [`ops-runbook.md`](ops-runbook.md) | docs | **Done** | — |
| 5 | [CR-99](https://linear.app/community-rule/issue/CR-99/backend-cloudron-production-install-apex-cutover) | — (ops; maintenance window + §6.1 export) | ops | Backlog | CR-98 green |
| 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:** **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.
**CR-102** (legacy rules Gitea export) runs during the CR-99 cutover window
([§6.1](ops-backend-deploy.md#61-legacy-rules-archive-cr-102)).
**Per-ticket detail:**
@@ -705,7 +705,7 @@ cutover window.
- **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.
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.** **Done** — [`docs/guides/ops-runbook.md`](ops-runbook.md). Covers deploy, rollback, restore drill, single-instance limits.
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.
---
@@ -848,10 +848,10 @@ Tickets **1011** can be deferred without blocking the core “auth + drafts +
| 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 | **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 | **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.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.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; includes §6.1 export |
| 12.5 | [CR-100](https://linear.app/community-rule/issue/CR-100/backend-steady-state-operator-runbook) | Steady-state operator runbook | **Done** — [ops-runbook.md](ops-runbook.md) |
| 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.7 | [CR-102](https://linear.app/community-rule/issue/CR-102/backend-decide-fate-of-legacy-rules-table-read-only-export) | Legacy `rules` table fate / export | **Parallel** — before CR-99 |
| 12.7 | [CR-102](https://linear.app/community-rule/issue/CR-102/backend-decide-fate-of-legacy-rules-table-read-only-export) | Legacy rules Gitea archive export | Ops — during CR-99 window (§6.1) |
| 13 | [CR-84](https://linear.app/community-rule/issue/CR-84/backend-api-error-contract-request-id-logging) | API errors + request-id logging | — |
| 14 | [CR-85](https://linear.app/community-rule/issue/CR-85/backend-custom-session-lifecycle-cleanup-invalidation-policy) | Session lifecycle + cleanup **Done** | — |
| 15 | [CR-86](https://linear.app/community-rule/issue/CR-86/backend-profile-dashboard-account-figma-profile) | Profile + account (Figma 22143:900069) | — |
+1 -1
View File
@@ -221,7 +221,7 @@ npm run dev
**Optional QA:** Run automated tests against an **ephemeral** database in CI instead of maintaining a fourth long-lived server.
**Target platform:** **Cloudron at MEDLab** — same host as the legacy [`CommunityRule/CommunityRuleBackend`](https://git.medlab.host/CommunityRule/CommunityRuleBackend) (Express + MySQL). The new app is packaged as a proper Cloudron app (Docker image + `CloudronManifest.json`, **postgresql + sendmail + localstorage** addons). Cloudron's container supervisor replaces the legacy 30-min `run.sh` watchdog. Admin handoff (access, env vars, platform settings, open decisions): [`docs/guides/ops-backend-deploy.md`](ops-backend-deploy.md). The app reads Cloudron-injected `CLOUDRON_POSTGRESQL_URL` and `CLOUDRON_MAIL_SMTP_*` via [`lib/server/env.ts`](../../lib/server/env.ts) (CR-96).
**Target platform:** **Cloudron at MEDLab** — same host as the legacy [`CommunityRule/CommunityRuleBackend`](https://git.medlab.host/CommunityRule/CommunityRuleBackend) (Express + MySQL). The new app is packaged as a proper Cloudron app (Docker image + `CloudronManifest.json`, **postgresql + sendmail + localstorage** addons). Cloudron's container supervisor replaces the legacy 30-min `run.sh` watchdog. First-time install and cutover: [`docs/guides/ops-backend-deploy.md`](ops-backend-deploy.md). Steady-state deploy, rollback, and restore drill: [`docs/guides/ops-runbook.md`](ops-runbook.md). The app reads Cloudron-injected `CLOUDRON_POSTGRESQL_URL` and `CLOUDRON_MAIL_SMTP_*` via [`lib/server/env.ts`](../../lib/server/env.ts) (CR-96).
**Admin / infra (coordinate with whoever runs the server):**
+64 -15
View File
@@ -131,11 +131,13 @@ apex.
only step with brief downtime (~515 min). Sequence:
1. Take one final manual backup of the legacy LAMP app (Cloudron
*Backups* tab → *Backup now*).
2. `cloudron uninstall` the legacy app at `communityrule.info`.
3. `cloudron configure --location communityrule.info` to move the
2. Export legacy `rules` + `version_history` to the Gitea archive
per [§6.1](#61-legacy-rules-archive-cr-102).
3. `cloudron uninstall` the legacy app at `communityrule.info`.
4. `cloudron configure --location communityrule.info` to move the
validated staging install to the apex (or `cloudron install`
fresh at apex if cleaner).
4. Re-run `prisma migrate deploy`, re-set production env vars if
5. Re-run `prisma migrate deploy`, re-set production env vars if
not preserved by the move, smoke again.
4. **Decommission** — see [CR-101](https://linear.app/community-rule/issue/CR-101/backend-decommission-legacy-expressmysql-backend).
Hold the final LAMP backup ≥ 90 days for safety.
@@ -155,11 +157,11 @@ Product decisions (closed):
1. **Final URL — `communityrule.info` apex.** New app fully replaces
the legacy site, including the marketing surface. Brief cutover
downtime (~515 min) is accepted.
2. **Legacy `rules` data — not migrated.** No data moves into the new
app's Postgres. A pre-cutover **read-only export** of the
`rules` + `version_history` MySQL tables is under consideration;
approach depends on the actual row count, which we'll pull as
part of the CR-99 pre-cutover backup. Tracked in
2. **Legacy `rules` data — not migrated; exported to Gitea.** No data
moves into the new app's Postgres. Before CR-99 uninstalls the
legacy MySQL, operators export the `rules` + `version_history`
tables to a new read-only Gitea repo on `git.medlab.host` (see
[§6.1](#61-legacy-rules-archive-cr-102)). Tracked in
[CR-102](https://linear.app/community-rule/issue/CR-102/backend-decide-fate-of-legacy-rules-table-read-only-export).
Infra decision closed:
@@ -184,6 +186,50 @@ Infra decision closed:
Container Registry** app and re-tag against its hostname; no other changes
required.
### 6.1 Legacy rules archive (CR-102)
The legacy Express backend stores published rules in bundled MySQL
tables `rules` and `version_history` (soft-delete via a `deleted`
column). These do not map to the new app's Postgres schema and are
**not imported**. Instead, a one-time export preserves the library for
posterity and operator lookup.
**Archive repo:** create
[`CommunityRule/legacy-rules-archive`](https://git.medlab.host/CommunityRule/legacy-rules-archive)
on `git.medlab.host` (same org as the other CommunityRule repos).
Mark the repo **archived** after the cutover push.
**When:** during the [CR-99](https://linear.app/community-rule/issue/CR-99/backend-cloudron-production-install-apex-cutover)
maintenance window — after the final Cloudron backup (§5 phase 3 step
1) and **before** `cloudron uninstall` (step 3).
**Export steps (operator):**
1. From the legacy LAMP container or a restored backup, pull row counts
for `rules` and `version_history` (include non-deleted vs soft-deleted
if useful for the README summary).
2. `mysqldump` both tables to SQL files (`rules.sql`,
`version_history.sql`).
3. Derive human-readable exports (JSON and/or CSV) from the dump for
anyone browsing the archive without MySQL tooling.
4. Commit artifacts + a `README.md` to the archive repo. The README
should record:
- cutover date;
- row counts and a brief activity summary;
- a short field glossary (`deleted`, version rows, etc.);
- a pointer to the new app at `communityrule.info`.
5. Tag the commit (e.g. `legacy-rules-YYYY-MM-DD`) and archive the
Gitea repo.
**Safety net:** the final Cloudron LAMP backup is retained ≥ 90 days
([CR-101](https://linear.app/community-rule/issue/CR-101/backend-decommission-legacy-communityrule-lamp-app))
for operator recovery if manual lookup from the export is ever needed.
**User discoverability:** link to the archive repo (or a release
download) from the new app — footer, help page, or a static
`/legacy-archive` page — so users looking for pre-cutover rules can
find it without knowing Gitea exists.
## 7. Old vs new deltas
So nothing surprises anyone at cutover:
@@ -231,19 +277,20 @@ All filed in Linear, titled `[Backend] …`, assigned to me, in the
4. [**CR-99**](https://linear.app/community-rule/issue/CR-99/backend-cloudron-production-install-apex-cutover)
— `[Backend] Cloudron production install + apex cutover`.
Side-by-side cutover at scheduled low-traffic window per §5.
Blocked by CR-98 green + CR-102 resolved.
Blocked by CR-98 green. Includes legacy rules export (§6.1) before
uninstall.
5. [**CR-100**](https://linear.app/community-rule/issue/CR-100/backend-steady-state-operator-runbook)
— `[Backend] Steady-state operator runbook`. Blocked by CR-98
(write what we actually did).
— `[Backend] Steady-state operator runbook` (**Done** —
[`ops-runbook.md`](ops-runbook.md)).
6. [**CR-101**](https://linear.app/community-rule/issue/CR-101/backend-decommission-legacy-communityrule-lamp-app)
— `[Backend] Decommission legacy CommunityRule LAMP app`.
Uninstall the entire LAMP slot (marketing + Express backend +
chatbot in one go); preserve final backup ≥ 90 days. Blocked by
CR-99 + sign-off window. Priority: Low.
7. [**CR-102**](https://linear.app/community-rule/issue/CR-102/backend-decide-fate-of-legacy-rules-table-read-only-export)
— `[Backend] Decide fate of legacy rules table (read-only export?)`.
Count rows + decide whether to publish a static archive before
CR-99 uninstalls the legacy MySQL. Priority: Low.
— `[Backend] Legacy rules archive export`. Decision: export to Gitea
(§6.1). Execute during the CR-99 maintenance window before
uninstall. Priority: Low.
## 9. Build and push image workflow
@@ -440,10 +487,12 @@ apex cutover.
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, see [`ops-runbook.md` §6](ops-runbook.md#6-single-instance-limitations).
## 12. Related docs
- [`docs/guides/ops-runbook.md`](ops-runbook.md) — steady-state deploy,
rollback, restore drill, single-instance limits ([CR-100](https://linear.app/community-rule/issue/CR-100/backend-steady-state-operator-runbook)).
- [`docs/guides/backend-roadmap.md`](backend-roadmap.md) §11
(environments) and §8 (Prisma migrations policy).
- [`docs/guides/backend-linear-tickets.md`](backend-linear-tickets.md)
+274
View File
@@ -0,0 +1,274 @@
# Steady-state operator runbook
Day-to-day deploy, rollback, and recovery for CommunityRule on MEDLab
Cloudron. Assumes staging or production is already installed and smoke-tested.
> **First-time install, apex cutover, and legacy decommission** live in
> [`ops-backend-deploy.md`](ops-backend-deploy.md). Use this doc once an
> environment is already running.
## 1. Quick reference
| Item | Value |
| ---- | ----- |
| Cloudron dashboard | `https://my.medlab.host` |
| Cloudron CLI login | `cloudron login my.medlab.host` |
| Staging app | `staging.communityrule.info` |
| Production app | `communityrule.info` (after apex cutover) |
| Container image | `git.medlab.host/communityrule/community-rule:<tag>` |
| Health check | `GET /api/health``200 {"ok":true,"database":"connected"}` |
| Manifest version | [`CloudronManifest.json`](../../CloudronManifest.json) `version` field (must increase for each release) |
| Current manifest | `0.1.8` at time of writing — always read the file before deploying |
Replace `<app>` below with the Cloudron location (`staging.communityrule.info`
or `communityrule.info`).
## 2. Prerequisites (one-time per operator)
1. **Cloudron access** — admin login on `my.medlab.host` and a CLI API token
(*Profile → API Tokens* on the dashboard). Save the token in 1Password.
2. **Cloudron CLI** — logged in:
```bash
cloudron login my.medlab.host
```
3. **Docker + buildx** — Docker Desktop or equivalent with `docker buildx`.
4. **Gitea registry auth** — personal access token on `git.medlab.host` with
`read:package` + `write:package`; then:
```bash
docker login git.medlab.host
```
5. **Repo checkout** — clone
[`CommunityRule/community-rule`](https://git.medlab.host/CommunityRule/community-rule)
and work from a clean commit that matches the release you intend to ship.
Images are **`linux/amd64` only** (Cloudron host is x86_64). On Apple
Silicon, the release script still builds amd64 via buildx; a bare
`docker pull` without `--platform linux/amd64` failing on arm64 is expected.
## 3. Deploy a new version
Typical release flow: bump manifest → build/push image → `cloudron update`
→ smoke.
### 3.1 Build and push
1. Check out the commit to release (`main` or a release branch).
2. **Bump** [`CloudronManifest.json`](../../CloudronManifest.json) `version`
(e.g. `0.1.8` → `0.1.9`). Cloudron requires the manifest version to
**increase** for `cloudron update --image` to be accepted.
3. From the repo root, build and push (tag should match the manifest version
for sanity):
```bash
TAG=0.1.9 ./scripts/docker-release.sh
# equivalent:
TAG=0.1.9 npm run docker:release
```
Omit `TAG=` to push `git rev-parse --short HEAD` instead — only do that
for ad-hoc staging experiments; production releases should use semver tags
aligned with the manifest.
4. **Verify anonymous pull** (simulates Cloudron):
```bash
docker logout git.medlab.host
docker pull --platform linux/amd64 \
git.medlab.host/communityrule/community-rule:0.1.9
```
5. **Commit the manifest bump** in git alongside the code that shipped in
this build.
Registry details and one-time Gitea setup: [`ops-backend-deploy.md` §9](ops-backend-deploy.md#9-build-and-push-image-workflow).
### 3.2 Update Cloudron
```bash
cloudron update --app staging.communityrule.info \
--image git.medlab.host/communityrule/community-rule:0.1.9
```
Use `communityrule.info` for production. Cloudron pulls the image (no registry
credentials on the host), restarts the container, and runs
[`scripts/start.sh`](../../scripts/start.sh), which:
1. `chown`s `/app/data` (localstorage mount),
2. runs **`prisma migrate deploy`**,
3. execs the Next.js standalone server.
Watch the app **Logs** tab in the Cloudron dashboard for a clean migration and
`Listening on port 3000`.
### 3.3 Migrations
**Normal case:** migrations apply automatically on container start — no
separate step.
**Manual re-run** (only if debugging a failed deploy or verifying before
traffic):
```bash
cloudron exec --app staging.communityrule.info -- npm run db:deploy
```
(`npm run db:deploy` → `prisma migrate deploy`.)
**Policy:** never run `prisma migrate reset` against staging or production.
Never edit migration files already applied to a shared database. Fix schema
drift by adding a **new** migration locally (`prisma migrate dev`) and
deploying a new image. See [`backend-roadmap.md` §8](backend-roadmap.md#8-prisma-migrations-policy).
### 3.4 Seed data (not every deploy)
Template + facet seed (`MethodFacet` rows for create-flow “Recommended” tags)
is **not** applied at boot. Run once per environment after first install, or
when recommendations return all-zero scores:
```bash
cloudron exec --app staging.communityrule.info -- \
node prisma/seed.bundle.cjs
```
Re-running is safe (idempotent upserts). JSON lives at `/app/seed-data/` in
the image — not under `/app/data` (Cloudron localstorage overwrites that
mount).
### 3.5 Smoke after deploy
**Automated** (from your laptop, repo root):
```bash
./scripts/staging-smoke.sh staging.communityrule.info
# production:
./scripts/staging-smoke.sh communityrule.info
# optional — exercises magic-link request (check inbox manually):
EMAIL=you@example.com ./scripts/staging-smoke.sh staging.communityrule.info
```
**Manual** (still required for full acceptance):
- Click a magic link → signed in → `GET /api/auth/session` returns a user.
- Publish a rule end-to-end → public detail page loads.
- Optional: Save & Exit draft sync; upload with `UPLOAD_ROOT` set.
Full checklist and failure table: [`ops-backend-deploy.md` §10](ops-backend-deploy.md).
## 4. Roll back (code-only)
To revert application code without touching the database:
```bash
cloudron update --app staging.communityrule.info \
--image git.medlab.host/communityrule/community-rule:<previous-tag>
```
Pick a tag you know was healthy (previous manifest version or git tag recorded
at last good deploy).
**Database implications:**
- Rolling back the **image** does **not** undo migrations already applied.
- If the bad release added a migration, rolling back to an older image may
leave the DB schema **ahead** of what that code expects — usually safe if
the migration was additive (new nullable columns, new tables).
- If the bad release broke because of a **destructive or incompatible**
migration, do **not** reset production. Restore from a Cloudron backup
(§5) or fix forward with a corrective migration.
**Never** `prisma migrate reset` on staging or production.
## 5. Restore drill (quarterly)
Verify Cloudron backups are restorable without touching the live app.
**Cadence:** at least once per quarter, or after any backup-policy change.
**Steps:**
1. In the Cloudron dashboard, pick a recent automatic backup of
`<app>` (*Backups* tab).
2. **Restore to a scratch location** — e.g.
`restore-drill-YYYYMMDD.communityrule.info` — not over the live app.
3. After restore completes, confirm the container starts and migrations are
current:
```bash
curl -sS "https://restore-drill-YYYYMMDD.communityrule.info/api/health"
```
Expect `200` with `"database":"connected"`.
4. Optional: `cloudron exec --app restore-drill-YYYYMMDD.communityrule.info -- npm run db:deploy`
if logs show pending migrations on an older snapshot.
5. Run `./scripts/staging-smoke.sh restore-drill-YYYYMMDD.communityrule.info`.
6. **Uninstall** the scratch app when done.
Record the drill date and outcome in your ops notes. Cloudron retains
automatic backups per platform defaults; confirm retention in the dashboard.
## 6. Single-instance limitations
The current Cloudron deploy runs **one container per environment**. Do not
scale to multiple app instances without addressing these per-process limits:
### 6.1 In-memory rate limiter
[`lib/server/rateLimit.ts`](../../lib/server/rateLimit.ts) stores windows in
process memory. Limits apply **per container**, not globally across instances.
| Route / action | Key | Min interval |
| -------------- | --- | ------------ |
| Magic-link request | per email | 60 s |
| Magic-link request | per IP | 20 s |
| Email change request | per email / IP / user | 60 s |
| Organizer inquiry | per email / IP | 60 s / 20 s |
| Publish with stakeholder invites | per IP | 60 s |
| Stakeholder add / resend | per IP / invite | 60 s |
| File upload | per user | 5 s |
Before horizontal scale-out, replace with a shared store (e.g. Redis) or edge
rate limits. See [`backend-roadmap.md` §5](backend-roadmap.md#5-session-and-authentication-v1).
### 6.2 Web vitals storage
Production defaults to **`external`** mode: vitals are structured log lines, not
written to Postgres or local files. Setting `WEB_VITALS_STORAGE=local` uses a
**per-process** file store under `.next/web-vitals` — suitable for dev/admin
only, not multi-instance. See [`backend-roadmap.md` §7](backend-roadmap.md#7-api-responses-errors-and-observability).
## 7. Environment variables (steady-state)
Cloudron **auto-injects** addon vars (`CLOUDRON_POSTGRESQL_URL`,
`CLOUDRON_MAIL_SMTP_*`). Operators set these manually once per app; they
persist across image updates unless changed:
| Variable | Purpose |
| -------- | ------- |
| `SESSION_SECRET` | Session cookie signing (≥ 16 chars). Rotating logs everyone out. |
| `SMTP_FROM` | Visible From on sign-in emails (e.g. `Community Rule <hello@communityrule.info>`). |
| `NEXT_PUBLIC_ENABLE_BACKEND_SYNC` | `true` in staging/production — Postgres draft persistence. |
| `UPLOAD_ROOT` | `/app/data/uploads` on Cloudron — required for file uploads. |
Full detail: [`ops-backend-deploy.md` §3](ops-backend-deploy.md#3-environment-variables).
## 8. Troubleshooting
| Symptom | Likely cause | Action |
| ------- | ------------ | ------ |
| Image pull error on update | Private repo, wrong tag, or amd64 manifest missing | Confirm repo is public; verify pull with `--platform linux/amd64` (§3.1) |
| Health `503` / `database: disconnected` | Postgres addon or `CLOUDRON_POSTGRESQL_URL` missing | Cloudron app → Environment |
| Container crash on start | Migration failure | App logs around `prisma migrate deploy`; fix forward with new migration |
| Magic link not sent | Mail addon or `SMTP_FROM` | Cloudron mail logs; `CLOUDRON_MAIL_SMTP_*` vars |
| Upload `server_misconfigured` | `UPLOAD_ROOT` unset | `cloudron env set --app <app> UPLOAD_ROOT=/app/data/uploads` |
| No “Recommended” on method cards | Seed not run | §3.4 — `node prisma/seed.bundle.cjs` |
| Rate limit too aggressive after deploy | Expected per §6.1 | Single instance only; limits reset on container restart |
App logs: Cloudron dashboard → *Logs* tab, or `cloudron logs --app <app> -f`.
## 9. Related docs
- [`ops-backend-deploy.md`](ops-backend-deploy.md) — first install, cutover
plan, legacy rules archive, build/push deep dive.
- [`backend-roadmap.md`](backend-roadmap.md) — migrations policy (§8),
rate limiting (§5), environments (§11).
- [`../relaunch-brief.md`](../relaunch-brief.md) — plain-language summary
for MEDLab admin.
- [`../../CONTRIBUTING.md`](../../CONTRIBUTING.md) — local dev setup.
+4 -3
View File
@@ -23,7 +23,7 @@ All three retire together when the new app goes live. The chatbot is **not** bei
## What does NOT carry over
- **No user accounts.** New sign-ins start fresh.
- **No published rules from the old database.** We'll count the existing `rules` table before cutover and decide whether to publish a read-only archive (CSV/JSON) somewhere for anyone looking for their old work.
- **No published rules from the old database.** Pre-cutover rules are exported to a read-only Gitea archive (`CommunityRule/legacy-rules-archive` on `git.medlab.host`); they are not imported into the new app. See [`docs/guides/ops-backend-deploy.md`](guides/ops-backend-deploy.md) §6.1.
- **No chatbot.**
## How the cutover will work
@@ -35,7 +35,7 @@ until the new one is verified.
`staging.communityrule.info` (auto-provisioned by Cloudron). Legacy app at the apex is not touched. Quiet testing within MEDLab/stakeholders.
2. **Cutover phase.** When staging is green and we're ready, schedule a low-traffic window. During the window (roughly 515 minutes of apex downtime):
- Take a final backup of the legacy app (Cloudron one-click).
- Pull a copy of the legacy `rules` table if we decided to publish an archive.
- Export the legacy `rules` + `version_history` tables to the Gitea archive (see ops-backend-deploy §6.1).
- Uninstall the legacy app at the apex `communityrule.info`.
- Move the new app to the apex.
- Smoke-test, confirm backups are on, done.
@@ -53,6 +53,7 @@ Roughly this order:
3. **Install at staging** subdomain, smoke test, soft launch (CR-98).
4. **Apex cutover window** — the brief downtime above.
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
([`ops-runbook.md`](guides/ops-runbook.md), CR-100).~~ **Done.**
Staging should be ready to deploy in 1-2 weeks, and we can go from there.