12 KiB
Backend deploy — admin handoff + cutover plan
This doc captures everything needed to deploy the new CommunityRule
(Next.js + Postgres) onto MEDLab's Cloudron and replace the legacy
LAMP-packaged service at communityrule.info. Cloudron admin access
has been granted; remaining open item is the registry decision in §6.
For a plain-language summary to hand to MEDLab's Cloudron admin, see
../relaunch-brief.md. This doc is the technical version.
1. Context
- This app fully replaces the existing
communityrule.infoservice — both the marketing site and the backend API. - The existing service is a single Cloudron LAMP app
(
lamp.cloudronapp.php74@5.1.2, installed at thecommunityrule.infoapex, 512 MiB) that hosts three things stuffed into one container under/app/data/public/:- The static marketing site (HTML / CSS / images).
- The Express/MySQL backend at
CommunityRule/CommunityRuleBackend, kept alive by a 30-minlsof-basedrun.shwatchdog on port 3000. MySQL is the LAMP package's bundled MySQL, persisted inside/app/data(not a Cloudron addon). - A Flask chatbot at
CommunityRule/CommunityRuleChatBoton port 5000, also watchdog-supervised; currently crash-looping withModuleNotFoundError: No module named 'flask'and last touched in May 2024. Not migrated. Dies with the LAMP container at decommission.
- The new app is a properly packaged Cloudron app (Docker image +
CloudronManifest.json, postgresql + sendmail + localstorage addons). Cloudron's container supervisor replaces the watchdog. - Greenfield Postgres. No data migration from the LAMP container's
internal MySQL. Old auth (4-digit OTP in
email_otp) is replaced by hashed magic-link tokens. Old API andrules/version_historytables do not map to anything in the new app.
2. Access — granted
Cloudron admin login on cloud.medlab.host granted. From the
dashboard the deployer can self-serve:
- Cloudron admin login (full admin on the MEDLab instance).
- DNS for
communityrule.info— domain is managed inside Cloudron, so new subdomains and TLS certs are one-click. - App log access — Cloudron web log viewer.
- Read of legacy app config — visible in admin UI.
cloudronCLI token — generate at Profile → API Tokens before first install. Save in 1Password.
3. Environment variables
Cloudron auto-injects (provisioned by addons declared in CloudronManifest.json)
Cloudron addons are not "enabled" platform-wide; they are requested per-app in the manifest and provisioned at install time.
CLOUDRON_POSTGRESQL_URL— from the postgresql addon. The app reads this name directly (Prisma +lib/server/env.ts).CLOUDRON_MAIL_SMTP_SERVER/_PORT/_USERNAME/_PASSWORD— from the sendmail addon. The platform Mail server is configured forcommunityrule.infowith Amazon SES relay + "allow custom from address" on, soSMTP_FROMof our choice will deliver. The app assembles a Nodemailer transport URL from these four vars inlib/server/env.ts.
I set manually via cloudron configure --app <id> --set-env
SESSION_SECRET— long random (openssl rand -hex 32). Required, ≥ 16 chars. Rotating it logs everyone out.SMTP_FROM— visible "From:" address on sign-in emails. Cloudron does not inject this. Usehello@communityrule.info(continuity with the legacy service; SES relay accepts it).NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true— turns on Postgres draft persistence for signed-in users. Required in production.UPLOAD_ROOT— absolute path to a writable directory (typically on the Cloudron localstorage mount) forPOST /api/uploads(community photo + custom-method attachments). When unset, upload routes returnserver_misconfigured. See CONTRIBUTING.md API table.
4. Platform settings
- Container
httpPort: 3000 (matchesDockerfileENV PORT=3000). - Health-check path:
/api/health(app/api/health/route.tsreturns200 {"ok":true,"database":"connected"}when healthy,503otherwise). - Memory limit: start at 512 MiB (matches what the legacy LAMP app has been running fine on for two years); raise if Next.js standalone OOMs under load.
- Backups: Cloudron's automatic backups are already on for the host (legacy app shows weekly snapshots ~451 MB each). Same default applies to new apps.
- TLS / DNS / SPF / DKIM: handled by Cloudron for any subdomain of
communityrule.info.
5. Cutover plan (side-by-side, never in-place)
The legacy app is at the apex communityrule.info and is still
serving real traffic. Best practice is side-by-side cutover — new
app gets validated at a fresh subdomain before any swap touches the
apex.
Phases
- Staging install —
cloudron install --image <our-image> --location staging.communityrule.info. Set env vars from §3. Runprisma migrate deploy. Smoke per CR-98. - Soft launch / acceptance — share the staging URL with a small group, exercise sign-in + publish + draft sync end-to-end. Hold here until confident.
- Apex cutover at a scheduled low-traffic window — this is the
only step with brief downtime (~5–15 min). Sequence:
- Take one final manual backup of the legacy LAMP app (Cloudron Backups tab → Backup now).
cloudron uninstallthe legacy app atcommunityrule.info.cloudron configure --location communityrule.infoto move the validated staging install to the apex (orcloudron installfresh at apex if cleaner).- Re-run
prisma migrate deploy, re-set production env vars if not preserved by the move, smoke again.
- Decommission — see CR-101. Hold the final LAMP backup ≥ 90 days for safety.
Why not in-place?
Uninstalling the legacy app and installing the new one at apex without a staging step means the live site is down for the entire duration of the first install — and the first install is exactly when all the env-var / addon / port surprises happen. Side-by-side keeps those surprises out of view.
6. Decisions — status
Product decisions (closed):
- Final URL —
communityrule.infoapex. New app fully replaces the legacy site, including the marketing surface. Brief cutover downtime (~5–15 min) is accepted. - Legacy
rulesdata — not migrated. No data moves into the new app's Postgres. A pre-cutover read-only export of therules+version_historyMySQL 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 CR-102.
Infra decision still open:
- Container registry — GHCR (under your personal / org account, lowest friction), Docker Hub, or MEDLab self-hosted. Tracked in CR-97.
7. Old vs new deltas
So nothing surprises anyone at cutover:
- Legacy is a LAMP package with bundled MySQL inside the container. New app uses the Cloudron postgresql + sendmail + localstorage addons — entirely different storage, no shared state.
- Legacy stuffs three apps (marketing + Node backend + Python
chatbot) into one container with a
run.shwatchdog. New app is one Next.js process, supervised by Cloudron natively. - Old auth = plaintext 4-digit OTP. New auth = hashed magic link in email. If users report "I'm not getting a code," remind them to look for a link instead.
- Old code hardcoded
from: 'hello@communityrule.info'incontrollers/emailController.jsbecause Cloudron does not inject aMAIL_FROM. New app readsSMTP_FROM— see §3. - Old API surface (
/api/send_otp,/api/publish_rule, etc.) and schema (rules+version_historytables, soft-delete viadeletedcolumn) do not overlap with the new app. No data migration. - The Flask chatbot at
CommunityRule/CommunityRuleChatBotis currently crash-looping inside the LAMP container and is not being migrated — confirmed with admin. It dies when the LAMP container is uninstalled in CR-101.
8. Follow-up tickets
All filed in Linear, titled [Backend] …, assigned to me, in the
Community-rule team, Backlog state.
- CR-96
—
[Backend] Cloudron-native env vars(shipped: app readsCLOUDRON_POSTGRESQL_URLandCLOUDRON_MAIL_SMTP_*only). - CR-97
—
[Backend] Container image registry: choose, build, push. Blocked by registry decision (§6.3). - CR-98
—
[Backend] Cloudron staging install + smokeatstaging.communityrule.info. Blocked by CR-96 + CR-97. - CR-99
—
[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. - CR-100
—
[Backend] Steady-state operator runbook. Blocked by CR-98 (write what we actually did). - CR-101
—
[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. - CR-102
—
[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.
10. Rate limiting (single-instance deploys)
The app uses an in-memory rate limiter in 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).
11. Related docs
docs/guides/backend-roadmap.md§11 (environments) and §8 (Prisma migrations policy).docs/guides/backend-linear-tickets.mdTicket 12 / CR-83 — this doc satisfies it.CONTRIBUTING.md— local dev setup (Postgres, magic-link, draft sync).