feat: add 9Router template - #1046
Conversation
built with Refined Cloudflare Pages Action⚡ Cloudflare Pages Deployment
|
7301e58 to
7dfd2fb
Compare
|
Retargeted this from |
|
Self-correction before review: the |
There was a problem hiding this comment.
Pull request overview
Adds a new Dokploy blueprint under blueprints/9router/ to deploy 9Router (AI model router / OpenAI-compatible proxy), including the Compose stack, Dokploy template configuration, and metadata/logo for indexing and UI display.
Changes:
- Added
docker-compose.ymldefining the9routerservice (pinned todecolua/9router:0.5.40) plus theheadroomsidecar. - Added
template.tomlwith generated secrets and domain/env wiring for Dokploy. - Added
meta.jsonand a PNG logo so the template can be listed and discovered.
Reviewed changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| blueprints/9router/docker-compose.yml | Defines the 9Router + headroom services, images, env, and persistence volume. |
| blueprints/9router/template.toml | Adds Dokploy variables, env injection, and domain routing to the 9router service port. |
| blueprints/9router/meta.json | Registers template metadata (id/name/version/links/tags/logo). |
| blueprints/9router/9router.png | Adds the template logo asset referenced by meta.json. |
| BASE_URL: https://${ROUTER_HOST} | ||
| HEADROOM_URL: http://headroom:8787 | ||
| AUTH_COOKIE_SECURE: "true" |
|
One more self-correction after reading It says:
I had so this is now consistent with the repo convention and with what 9Router itself documents, rather than me second-guessing it. Two other things I checked against that file while I was there, so they're not left implicit:
On the HTTP/HTTPS point I should flag that the repo is genuinely split: 134 occurrences use
|
| JWT_SECRET: ${JWT_SECRET} | ||
| INITIAL_PASSWORD: ${INITIAL_PASSWORD} | ||
| API_KEY_SECRET: ${API_KEY_SECRET} | ||
| MACHINE_ID_SALT: ${MACHINE_ID_SALT} | ||
| BASE_URL: http://${ROUTER_HOST} | ||
| HEADROOM_URL: http://headroom:8787 | ||
| AUTH_COOKIE_SECURE: "false" |
|
Two comments here — one I'd already fixed, one that sent me looking and turned up a genuine bug next to it. 1. 2. No But chasing that list found something I had actually got wrong. Look at the last two entries: upstream reads Fixed in BASE_URL: http://${ROUTER_HOST}
NEXT_PUBLIC_BASE_URL: http://${ROUTER_HOST}So the second comment was right that something was missing from the container environment — just a different variable than the one named. Thanks for the prompt. Re-verified after the change: |
| # Optional token-reduction proxy that 9Router calls through HEADROOM_URL. | ||
| # Shipped as a sidecar because the 9Router image does not bundle it. | ||
| headroom: | ||
| image: ghcr.io/chopratejas/headroom:0.6.7 | ||
| restart: unless-stopped |
|
Right, and thank you for catching it — the description was stale and actively misleading, which is worse than it being merely out of date. To confirm the factual part first: The sequence was: I opened this PR claiming the package "publishes only `latest`" and therefore could not be pinned. That claim was wrong — when a Copilot review on #1048 pointed me at the The description is now rewritten. Both images are described as pinned with their verified platforms, and I've added a "Corrections made during review" section listing all four changes this PR went through (retarget to |
What
Adds a
9routertemplate, implementing the request in #1040. The issue author noted they would not be sending a PR themselves.9Router (23.5k stars, MIT) is a self-hosted AI model router: it rotates requests across 40+ providers with automatic fallback and exposes an OpenAI-compatible endpoint that coding agents (Claude Code, Codex, Cursor, Cline) can point at.
Files
blueprints/9router/docker-compose.ymlblueprints/9router/template.tomlblueprints/9router/meta.jsonblueprints/9router/9router.pngHow it was built
Derived from upstream's own
docker-compose.yml,DOCKER.mdand.env.examplerather than written from scratch, then adapted to this repo's conventions:decolua/9router:0.5.40(Docker Hub,linux/amd64+linux/arm64, published 2026-07-20) andghcr.io/chopratejas/headroom:0.6.7(GHCR,linux/amd64+linux/arm64, manifest verified HTTP 200 with a pull token). Upstream ships both as:latest.ports:— upstream publishes20128:20128and8787:8787; the domain mapping intemplate.tomlhandles this instead.container_name:— upstream sets it on both services.env_file: .env, which does not exist in a Dokploy deployment; the required variables are supplied through[config.env].restart: always→unless-stoppedto match the existing blueprints.http://per the URL convention in.github/copilot-instructions.md, andAUTH_COOKIE_SECUREstays"false"— which is also what upstream's.env.exampleships.Secrets come from upstream's
.env.example, which documentsJWT_SECRETandINITIAL_PASSWORDas required, plusAPI_KEY_SECRETandMACHINE_ID_SALTas recommended. All four are generated with${password:N}, so no deployment ships a default credential.Both
BASE_URLandNEXT_PUBLIC_BASE_URLare wired to the assigned domain. The second one matters: it is the value the browser bundle uses, and without it the deployed UI falls back to upstream'shttp://localhost:20128and calls localhost instead of its own domain.The
headroomsidecar is kept because upstream's compose wiresHEADROOM_URLto it and the 9Router image does not bundle it.Corrections made during review
Recording these so the history is readable rather than quietly amended:
maintocanary.canaryis the default branch and 5 of the last 6 merged template PRs went there;mainwas 94 commits behind.headroomwas originally:latest. My first description claimed the package "publishes onlylatest" — that was wrong. Checking the GHCR tag list properly found 100 tags including plain semver, so it is now pinned to0.6.7.BASE_URLwas originallyhttps://withAUTH_COOKIE_SECURE: "true". Both now follow the repo's HTTP-by-default convention and upstream's own defaults.NEXT_PUBLIC_BASE_URLwas missing. Found while checking a review note aboutROUTER_HOST; see the thread.Testing
Beyond CI I validated the config files by parsing them rather than by eye:
template.tomlparses with a real TOML parser: 5 variables, 5 env entries, domain{serviceName: "9router", port: 20128, host: "${main_domain}"}—serviceNamematches the compose service name.docker-compose.ymlparses with a real YAML parser, and I asserted this repo's rules programmatically: no service declaresports, none declarescontainer_name, no custom networks.${VAR}in the compose file is provided bytemplate.toml:JWT_SECRET, INITIAL_PASSWORD, API_KEY_SECRET, MACHINE_ID_SALT, ROUTER_HOST— all five present.images/9router.png, resized 2940×2594 → 512×451 (758 KB → 108 KB).What I could not do: I have no Dokploy instance, so this has not been deployed and clicked through. The PR preview deploy will be the first real run. Given CONTRIBUTING asks contributors to test before submitting, I want to be straight that my verification is config-level, image-manifest and licence checks — not a live deployment. Happy to convert to draft if you would rather hold it until someone can boot it.