chore: add Render Blueprint for split frontend/backend deployment - #318
Conversation
Render.com's single-service "Dockerfile Path" form accepts only one Dockerfile per service, but Render Blueprints support many services per repository with their own dockerfilePath. This adds render.yaml that registers naruon-backend, naruon-frontend, and naruon-postgres as separate resources, matching the two-Dockerfile layout already used by docker-compose.yml and k8s/*.yaml. The split keeps the AGENTS.md boundary that the browser never holds backend secrets: the frontend talks to the backend through the same-origin /api/* rewrite, which now reads its destination at runtime from BACKEND_INTERNAL_URL (Render injects the backend's public URL; local Compose falls back to the existing 127.0.0.1:8000 loopback). Backend boot still goes through scripts/start_backend.py rather than uvicorn directly. The dockerCommand rewrites Render's postgresql:// URL to the postgresql+asyncpg:// driver form that backend/db/session.py needs, because Render Blueprints do not support variable interpolation and we will not add a code default for DATABASE_URL. docs/operations/render-deployment.md is the runbook for first-time setup, pgvector enablement (bootstrap_db.py attempts it automatically; falls back to a one-time psql command if the app role lacks the privilege), secret rotation, and silencing the IMAP worker before any tenant is configured. Co-Authored-By: Mastra Code (anthropic/claude-opus-4-7) <noreply@mastra.ai>
|
Warning Review limit reached
More reviews will be available in 7 minutes and 48 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (11)
📝 WalkthroughWalkthroughAdds a Render.com Blueprint for backend+frontend services and managed Postgres, implements environment-driven frontend /api/* rewrites with safety checks, updates Docker/CI build configs to pass BACKEND_INTERNAL_URL, provides a Render runbook and changelog entries, and expands Strix PR-scope gating logic and tests. ChangesRender.com Deployment Support
CI / Strix quick-gate
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
PR governance metadata gate is not ready for
|
Self-review summaryCI status: 14 SUCCESS / 1 SKIPPED (publish, expected on PR) / 0 FAILURE. CodeRabbit is rate-limited and will auto-retry; the Self-audit findings I corrected before pushing
Out-of-scope items I noticed but did not include
Verification done locally
Will wait for the CodeRabbit retry window before any further action. |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/operations/render-deployment.md`:
- Around line 96-98: The fenced code block that contains the environment
variable DISABLE_BACKGROUND_WORKERS=1 is missing a language tag and triggers
markdownlint MD040; update the block delimiter from ``` to ```bash so it becomes
a bash code fence (e.g., replace the opening ``` with ```bash) to explicitly
mark the snippet as shell/bash.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: af4f5b29-4f20-4aa5-8260-774e157a9777
📒 Files selected for processing (5)
CHANGELOG.mddocs/operations/render-deployment.mdfrontend/Dockerfilefrontend/next.config.tsrender.yaml
CodeRabbit flagged markdownlint MD040 on the env-var snippet in the 'Optional: disable background workers on first boot' section. The surrounding sql blocks already declare a language; aligning this one keeps the runbook lint-clean. Co-Authored-By: Mastra Code (anthropic/claude-opus-4-7) <noreply@mastra.ai>
Strix flagged next.config.ts's /api/* rewrite as a HIGH-severity SSRF sink (CVSS 8.9): an operator with environment-variable control could point BACKEND_INTERNAL_URL at AWS metadata (169.254.169.254), any RFC 1918 host, or an IPv6 ULA/link-local address and have Next.js proxy /api/* there. The same fail-closed posture that AGENTS.md requires for LLM provider base_url applies here. When BACKEND_INTERNAL_URL is set explicitly, the rewrite destination now passes through assertSafeBackendInternalUrl: HTTPS-only scheme check plus a hostname denylist covering IPv4 loopback (127/8), RFC 1918 (10/8, 172.16/12, 192.168/16), link-local (169.254/16), IPv4 unspecified (0/8), IPv6 loopback (::1) and unspecified (::), IPv6 ULA (fc00::/7), and IPv6 link-local (fe80::/10). Hostnames are normalized (lowercased, IPv6 brackets stripped) before matching, so mixed-case literals (LOCALHOST, FC00::1) and bracketed IPv6 forms are both rejected. When the variable is unset the loopback fallback 'http://127.0.0.1:8000' is kept on purpose so docker compose and local 'npm run dev' continue to work without configuration. Verified against 24 input cases: unset/empty fallback, Render-style https hosts (mixed-case, trailing slash, port, path prefix), every denied private/loopback range, IPv4 boundary values (172.15 and 172.32 correctly stay allowed), and an allowed IPv6 global (2001:db8::1). Co-Authored-By: Mastra Code (anthropic/claude-opus-4-7) <noreply@mastra.ai>
…re hosts behind opt-in Strix raised a second finding (MEDIUM, CVSS 4.1) against the loopback fallback in next.config.ts: even with the HTTPS+denylist guards in place, an unset BACKEND_INTERNAL_URL would still send /api/* through http://127.0.0.1:8000, which is reachable from any service co-located with the Next.js process. The same fail-closed reasoning applies here as for the LLM provider base_url policy in AGENTS.md. Resolution: * When NODE_ENV=production and BACKEND_INTERNAL_URL is unset, backendRewriteDestination() throws so the build/start fails immediately. Render's fromService.envVarKey populates it from naruon-backend's RENDER_EXTERNAL_URL, so production builds keep working end-to-end. * docker-compose can't satisfy the HTTPS+global-host rule because it reaches the backend over the Docker network with the service hostname (http://backend:8000). Added an explicit ALLOW_INSECURE_BACKEND_INTERNAL_URL=1 opt-in that relaxes the scheme + private-range checks for that single use case. Render deployments never set the flag and inherit the strict policy. * frontend/Dockerfile declares the two new env vars as ARGs so Compose can pass them through; CHANGELOG documents the rule. Verified six new cases on top of the previous twenty-four: production with valid HTTPS host, production missing var (throws), Compose-style http://backend:8000 with and without the opt-in flag, dev/test fallback (unchanged), and the explicit-bypass-to-metadata case that the opt-in intentionally permits when an operator chooses it. Co-Authored-By: Mastra Code (anthropic/claude-opus-4-7) <noreply@mastra.ai>
The SSRF fix makes production Next.js builds fail closed when BACKEND_INTERNAL_URL is missing. Application CI and Docker image validation build the frontend without Render's fromService env vars, so those jobs now provide the same HTTPS backend origin shape Render will inject in production. This keeps the fail-closed policy intact: CI uses a global HTTPS host, not the insecure compose-only opt-in, and frontend image validation now passes BACKEND_INTERNAL_URL as a Docker build arg for both PR validation and release publishing. Co-Authored-By: Mastra Code (anthropic/claude-opus-4-7) <noreply@mastra.ai>
The previous ALLOW_INSECURE_BACKEND_INTERNAL_URL flag relaxed all scheme and private-range checks, which left a static SSRF bypass shape for Strix to flag: setting BACKEND_INTERNAL_URL to a metadata or private host while also setting the flag. Replace it with ALLOW_DOCKER_BACKEND_INTERNAL_URL and allow exactly the Compose service URL http://backend:8000, with no path prefix. Every other explicit BACKEND_INTERNAL_URL still requires HTTPS and a global host, even when the Compose flag is present. Production builds still fail when BACKEND_INTERNAL_URL is missing, so Render must inject the backend RENDER_EXTERNAL_URL and local dev stays on the non-production loopback fallback. Verified the full matrix: Render HTTPS host passes; production missing var fails; development missing var falls back; http://backend:8000 passes only with ALLOW_DOCKER_BACKEND_INTERNAL_URL=1; metadata, wrong-port, and path-prefixed backend URLs are rejected even with the Compose flag. Co-Authored-By: Mastra Code (anthropic/claude-opus-4-7) <noreply@mastra.ai>
There was a problem hiding this comment.
This review was skipped because it would exceed your organization's monthly flex usage limit. Raise the limit in billing settings or wait until the next billing period resets limits.
The PR branch was based on an older trusted Strix gate script that rejected the new __PR_SCOPE__ sentinel. origin/master now passes that sentinel from the pull_request_target workflow and includes the matching scripts/ci/strix_quick_gate.sh support. Merging master brings the workflow and trusted gate script back into sync so Strix can evaluate this PR's changed-file scope instead of failing before the scan starts. Co-Authored-By: Mastra Code (anthropic/claude-opus-4-7) <noreply@mastra.ai>
origin/master advanced with the Strix whole-PR-scope fix after this PR last ran CI. Merging it keeps the branch current with the trusted pull_request_target workflow and gate scripts so governance no longer reports the branch as behind. Co-Authored-By: Mastra Code (anthropic/claude-opus-4-7) <noreply@mastra.ai>
After the latest master merge, Strix produced zero vulnerabilities but still failed closed because its PR-scope report said Dockerfile, frontend/Dockerfile, and VERSION were missing. The scanner was correct: deployment and workflow changes can reference build context files that are not changed in the PR, so a changed-file-only scope is too narrow. When PR-scoped changed files include GitHub workflows, Dockerfiles, frontend next config, docker-compose, or render.yaml, include trusted copies of the backend/frontend Dockerfiles, frontend package manifests, frontend PostCSS/Next config, docker-compose.yml, render.yaml, and VERSION in the isolated scan scope. This gives Strix enough build and release context while preserving the pull_request_target invariant that only PR-head blobs for changed files are copied from the untrusted head. Added a gate self-test assertion so deployment context cannot regress. Verified with bash scripts/ci/test_strix_quick_gate.sh. Co-Authored-By: Mastra Code (anthropic/claude-opus-4-7) <noreply@mastra.ai>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/docker-publish.yml:
- Around line 88-89: The workflow currently bakes
BACKEND_INTERNAL_URL=https://naruon-backend.onrender.com into the frontend image
causing next.config.ts rewrites() to be fixed to production; remove that
build-time constant and ensure the backend URL is provided at runtime or via
environment-specific builds. Concretely: stop passing BACKEND_INTERNAL_URL in
.github/workflows/docker-publish.yml (or set it only for production deploys),
update frontend/Dockerfile to avoid copying a build ARG into ENV
BACKEND_INTERNAL_URL at image build, and change frontend/next.config.ts (the
async rewrites() logic) to derive the target from a runtime-provided value (or
from NEXT_PUBLIC_API_URL) so the pushed image is not permanently wired to
production; alternatively publish separate images per environment that set
BACKEND_INTERNAL_URL only for that environment.
In `@frontend/next.config.ts`:
- Around line 24-37: The denylist misses IPv4-mapped IPv6 literals (e.g.
::ffff:127.0.0.1), so add regexes to DENIED_BACKEND_HOST_PATTERNS to match
IPv4-mapped forms for all private/loopback ranges (e.g. patterns for
/^::ffff:(?:0:)?127\./, /^::ffff:(?:0:)?10\./, /^::ffff:(?:0:)?192\.168\./,
/^::ffff:(?:0:)?172\.(1[6-9]|2\d|3[01])\./, /^::ffff:(?:0:)?169\.254\./, and
/^::1$/ already exists); update the array where DENIED_BACKEND_HOST_PATTERNS is
declared (and ensure normalizeHost still lowercases/strips brackets) so
BACKEND_INTERNAL_URL hostnames like [::ffff:127.0.0.1] are correctly matched and
denied.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 64973e3f-5e08-4d8f-8718-3f6cb2ec9d0a
📒 Files selected for processing (9)
.github/workflows/app-ci.yml.github/workflows/docker-publish.ymlCHANGELOG.mddocker-compose.ymldocs/operations/render-deployment.mdfrontend/Dockerfilefrontend/next.config.tsscripts/ci/strix_quick_gate.shscripts/ci/test_strix_quick_gate.sh
✅ Files skipped from review due to trivial changes (3)
- .github/workflows/app-ci.yml
- CHANGELOG.md
- docs/operations/render-deployment.md
…int-deployment-20260530
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai resolve |
✅ Actions performedComments resolved and changes approved. |
Summary
Add a Render.com Blueprint (
render.yaml) that registersnaruon-backend,naruon-frontend, andnaruon-postgresas separate Render resources, each pointing at the Dockerfile they already had in this repository. This unblocks deployment on hosts whose 'Dockerfile Path' setup form only accepts one Dockerfile per service, without merging the two containers or changing the two-Dockerfile layout used bydocker-compose.ymlandk8s/*.yaml.What changed
render.yaml(new) — Blueprint definition. Backend uses./Dockerfile, frontend uses./frontend/Dockerfile, both withdockerContext: .because the existing frontend Dockerfile copies from the repo root. Postgres is a managedbasic-256mbinstance.frontend/next.config.ts—/api/*rewrite destination is now driven byBACKEND_INTERNAL_URL. Trailing slashes and whitespace are normalized. If the variable is missing the function falls back to the existinghttp://127.0.0.1:8000loopback, sodocker compose upand localnpm run devcontinue to work with zero configuration.frontend/Dockerfile— comment-only annotation clarifying thatNEXT_PUBLIC_API_URLARG is kept for backwards compatibility but new code relies on the same-origin rewrite. No runtime change.docs/operations/render-deployment.md(new) — runbook covering Blueprint sync, pgvector enablement (bootstrap_db.pyattemptsCREATE EXTENSION IF NOT EXISTS vectorautomatically; falls back to a one-timepsqlstep if the app role lacks the privilege), secret rotation including the raregenerateValueplaceholder-term failure mode, and how to suppress the IMAP worker before any tenant is configured.CHANGELOG.md— "추가" entry under[Unreleased].AGENTS.md constraints preserved
python scripts/start_backend.py. The DockerfileCMDis unchanged;dockerCommandonly adds--host 0.0.0.0 --port $PORT.DATABASE_URLorAUTH_SESSION_HMAC_SECRET. Render injects them viafromDatabaseandgenerateValue: true.dockerCommand(sed rewrite), not by patching backend settings.Authorization: Bearersigned-session token path.Dockerfile,frontend/Dockerfile,docker-compose.yml, andk8s/*.yamlare unchanged.Test plan
render.yamlwithyaml.safe_loadconfirms the foldeddockerCommandparses to one shell-safe line and thatfromDatabase/fromServicereferences resolve to the expected service names.postgres://→postgresql+asyncpg://, idempotent for already-asyncpg URLs) confirmed.backendRewriteDestinationinnext.config.ts: unset / empty / plain URL / trailing slash / multiple trailing slashes / whitespace padding — all produce the expected destination.npm run typecheckreproduces the same 66 pre-existing test-file errors with and without this change; no new diagnostics introduced bynext.config.ts.Co-Authored-By: Mastra Code (anthropic/claude-opus-4-7) noreply@mastra.ai
Summary by CodeRabbit
Documentation
Chores
Bug Fixes / Security