feat(deploy): configurable nginx upstreams via env vars (#183)#189
Merged
Conversation
The dashboard image bundles an nginx reverse proxy that splits /v1/*
traffic between the governance plane (/v1/*) and the runtime plane
(/v1/reservations/*). The two upstream targets were hardcoded, so
retargeting them (split hosts, external endpoints) meant editing the
config and rebuilding the image.
Ship the config as an envsubst template (nginx.conf ->
default.conf.template) with ${ADMIN_UPSTREAM} / ${RUNTIME_UPSTREAM}
placeholders, rendered by the stock nginx entrypoint at container start.
Bake defaults (cycles-admin:7979 / cycles-server:7878) matching the
compose service names so existing deployments are unaffected; override
either var at deploy time with no file edit and no image rebuild.
This keeps the same-origin proxy model, so no CSP/CORS/key-exposure
changes are needed (unlike the VITE_*-driven cross-origin approach
proposed in the issue, declined in AUDIT.md).
- Dockerfile: copy template to /etc/nginx/templates/; ENV defaults
- docker-compose.prod.yml: document the two vars on the dashboard service
- CI: e2e.yml + pr-container-scan.yml path filters nginx.conf -> template
- Docs: README, OPERATIONS, AUDIT (+ baseline cycles-server .15 -> .17
fix), CHANGELOG, package.json 0.1.25.60 -> 0.1.25.61
Verified: real nginx:1.29-alpine3.23 entrypoint renders the template and
nginx -t passes; local envsubst replication confirms both upstreams fill
and nginx runtime vars ($host/$request_uri/$upstream) are preserved.
amavashev
added a commit
that referenced
this pull request
May 31, 2026
PR #189 bumped package.json to 0.1.25.61 but left the lockfile's top-level version at .60. Regenerated via `npm install --package-lock-only` — only the two version fields change, no dependency tree churn. Keeps the release-version alignment the release process requires before tagging.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #183.
Problem
The dashboard image bundles an nginx reverse proxy that splits
/v1/*traffic between the governance plane (/v1/*) and the runtime plane (/v1/reservations/*). The two upstream targets were hardcoded innginx.conf, so retargeting them (split hosts, external endpoints, non-default ports) meant editing the config and rebuilding the image.Change
Ship the proxy config as an
envsubsttemplate (nginx.conf→default.conf.template) with${ADMIN_UPSTREAM}/${RUNTIME_UPSTREAM}placeholders, rendered by the stock nginx entrypoint (20-envsubst-on-templates.sh) at container start. Defaults (http://cycles-admin:7979/http://cycles-server:7878) are baked into the image and match the compose service names, so existing deployments are unaffected. Override either var at deploy time — no file edit, no image rebuild:Why not the
VITE_*approach from the issueVITE_ADMIN_URL/VITE_RUNTIME_URLdriving direct cross-originfetchwas declined (seeAUDIT.md):VITE_*bakes at build time (one image per env, defeating the portable-artifact model), requires relaxing CSPconnect-src 'self', turns on mandatory CORS + preflight forX-Admin-API-Keyon both backends, and sends the admin key cross-origin. Keeping the same-origin bundled proxy avoids all four.Files
nginx.conf→default.conf.template— upstreams now placeholdersDockerfile— copy template to/etc/nginx/templates/; bakeENVdefaultsdocker-compose.prod.yml— document the two vars on thedashboardservicee2e.yml+pr-container-scan.ymlpath filtersnginx.conf→default.conf.templatecycles-server.15→.17fix), CHANGELOG,package.json0.1.25.60→0.1.25.61Validation
nginx:1.29-alpine3.23entrypoint with override upstreams; the entrypoint launched20-envsubst-on-templates.shandnginx -treported syntax OK / test successful on the rendered config.envsubststep locally: both upstreams fill, all nginx runtime vars ($host/$request_uri/$upstream/ …) are preserved, zero leftover${…}placeholders.src/change, so the Vitest suite is unaffected.Deployment-infra only — no spec change, no admin-API surface delta, no client behaviour change.