Agent Gateway on Fly.io
nyuchi/agentgateway is the agent
gateway all Nyuchi agents route through. It runs the
agentgateway Docker image on Fly.io
(app agentgateway, org nyuchi-web-services, region iad).
Live at fundi.nyuchi.com.
Repo layout
Section titled “Repo layout”| File | Does |
|---|---|
Dockerfile.vercel |
FROM cr.agentgateway.dev/agentgateway:v1.4.1, copies in config.yaml with --chown=65532:65532 (see below), runs agentgateway -f /config.yaml. Named .vercel for historical reasons — fly.toml’s [build] block still points at this exact filename. |
config.yaml |
config.adminAddr: "off", the single gateway listener, one routes entry per namespaced MCP server, the ui OIDC policy |
fly.toml |
One [[services]] block, one port (3000 internal → 80/443 external) |
FROM cr.agentgateway.dev/agentgateway:v1.4.1
COPY --chown=65532:65532 config.yaml /config.yaml
CMD ["-f", "/config.yaml"]One port, on purpose
Section titled “One port, on purpose”fly.toml declares exactly one [[services]] block, port 3000 internal,
forwarded externally on 80 (redirects to 443) and 443:
[[services]] internal_port = 3000 protocol = "tcp" auto_stop_machines = false auto_start_machines = true min_machines_running = 1
[[services.ports]] port = 80 handlers = ["http"] force_https = true
[[services.ports]] port = 443 handlers = ["tls", "http"]An earlier revision added a second [[services]] block exposing the
admin UI on its own port, 15000. It was reverted: verified working over
Fly’s internal WireGuard network, but the TLS handshake on 15000 reset
from every public-internet path tested. OIDC is what actually gates the
UI, not the port — a second exposed port bought no security and cost
real reachability. The base gateway on 443 now serves the proxy, the
namespaced MCP routes, and the OIDC-gated UI together.
Admin UI, gated by WorkOS OIDC
Section titled “Admin UI, gated by WorkOS OIDC”The ui block has no explicit gateways: key, so it falls back to the
default gateway — same port as everything else — and requires a browser
OIDC login before serving anything:
config: adminAddr: "off"
ui: policies: oidc: issuer: ${WORKOS_ISSUER} clientId: ${WORKOS_CLIENT_ID} clientSecret: ${WORKOS_CLIENT_SECRET} redirectURI: https://fundi.nyuchi.com/oauth/callbackconfig.adminAddr stays "off" — agentgateway’s dedicated admin port has
no built-in auth at all (adminAddr only controls where it binds, never
how it authenticates).
The ${WORKOS_*} tokens are not placeholders we substitute — agentgateway
resolves $VAR/${VAR} natively from its own process environment at
startup, scanning the whole config file. That’s confirmed the hard way: an
earlier revision tried routing secrets through a hand-rolled envsubst
entrypoint in a custom debian:bookworm-slim image, and a stray literal
${WORKOS_*} left in a comment made agentgateway try to look up an env
var literally named WORKOS_* and refuse to boot. Once that surfaced
agentgateway’s native substitution, the custom-image detour turned out to
be unnecessary.
The OIDC identity is a WorkOS Connect OAuth Application (first-party,
confidential — not Public, since agentgateway exchanges the code
server-side), created in the “Nyuchi Identity” WorkOS project’s Production
environment — the same pattern mzizi-mcp and bushtrade-mcp use for
gating internal services against the same identity pool.
Four secrets, set with flyctl secrets set, not written to config.yaml
or committed to git:
| Var | Value |
|---|---|
WORKOS_ISSUER |
The Connect OAuth Application’s issuer, e.g. https://identity.nyuchi.com |
WORKOS_CLIENT_ID |
Its Client ID |
WORKOS_CLIENT_SECRET |
Its Client Secret |
OIDC_COOKIE_SECRET |
64 hex chars (openssl rand -hex 32) — a 32-byte AES-256-GCM key |
With the OIDC policy active, agentgateway redirects every unauthenticated
request that doesn’t match a more specific route — including /, not
just /ui/* — to the WorkOS login. /docs/mcp is unaffected because it’s
an explicit route matched first.
Config durability
Section titled “Config durability”Fly machines have an ephemeral root filesystem. The --chown fix above
makes the admin UI’s live saves to /config.yaml succeed, but nothing
currently backs that file with a Fly Volume — any edit made through the UI
is lost on the next flyctl deploy or machine restart, both of which
revert to whatever’s baked into the image. Durable UI-driven config editing
needs a volume-backed config path; not done yet.
Namespaces: one gateway, many MCP servers
Section titled “Namespaces: one gateway, many MCP servers”The actual point of running an API gateway in front of the Nyuchi agents:
one domain, one path prefix per backend MCP server, instead of a different
worker hostname for each one. agentgateway’s mcp backend type is
protocol-aware — it terminates the MCP session itself, rather than doing a
raw reverse proxy:
routes: - name: docs-mcp matches: - path: pathPrefix: /docs/mcp backends: - mcp: targets: - name: nyuchi-docs-mcp mcp: host: https://nyuchi-docs-mcp.nyuchi.workers.dev/mcpThat puts nyuchi-docs-mcp at
fundi.nyuchi.com/docs/mcp, still reachable directly at
docs.nyuchi.com/mcp and nyuchi-docs-mcp.nyuchi.workers.dev/mcp — the
gateway is an additional front door, not a replacement:
curl -X POST https://fundi.nyuchi.com/docs/mcp \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"0.0.1"}}}'Every request past initialize needs the Mcp-Session-Id the gateway hands
back. Add another Nyuchi agent’s MCP server the same way: a new routes
entry, a new namespace, flyctl deploy.
Deploying
Section titled “Deploying”There’s no CI/CD wired up yet — every deploy so far has been manual from a checkout:
flyctl deploy -a agentgateway --local-only--local-only builds with the local Docker daemon instead of Fly’s remote
(Depot) builder. That matters in some sandboxed/proxied environments where
the remote builder’s gRPC connection can’t be tunneled through an HTTP
proxy — it isn’t required from a normal machine with unrestricted network
access, but it’s the flag that got a build through when the remote builder
timed out with a TLS handshake failure.
Networking
Section titled “Networking”- Domain:
fundi.nyuchi.com— Cloudflare DNS (nyuchi.comzone),A/AAAArecords to the app’s dedicated IPs, plus_fly-ownershipand_acme-challengerecords, following the same pattern already used forapi.nyuchi.comandauth.nyuchi.com. - Dedicated IPs: IPv4 ($2/mo) and IPv6 —
flyctl ips list -a agentgateway. Fly requires a dedicated IP once an app runs more than one[[services]]block; kept even after collapsing back to one, since it’s already allocated and paid for. - TLS: Let’s Encrypt, issued and managed by Fly
(
flyctl certs list/check/add -a agentgateway).
History
Section titled “History”Shipped on Vercel first — Dockerfile.vercel running as a Vercel
Function via container image support,
admin UI folded into the single port Vercel forwards, domain
mcp.nyuchi.com. Migrated to Fly.io for full multi-service control.
The Vercel project, its domain attachment, and all its deployments were
deleted after the migration; mcp.nyuchi.com was retired in favor of
fundi.nyuchi.com. Two detours along the way, both reverted: a standalone
debian:bookworm-slim + envsubst entrypoint (unnecessary — agentgateway
substitutes env vars into its own config natively), and a dedicated
admin-UI port on 15000 (unreachable from the public internet from every
network tested).
Cross-links
Section titled “Cross-links”nyuchi/agentgateway— the repo.- agentgateway.dev docs — upstream configuration reference (gateways, listeners, backends).
- Fly.io: Container Images / Machines — the underlying Fly.io deployment model.