---
module: platform.nginx-routing
title: NGINX Routing, CORS & Blue/Green
tab: Overview
order: 10
audience: dev
---

> **DRAFT · internal.** Written 2026-07-01. NGINX on the services VM (`173.231.195.250`) is the
> single entry point for every `*.bcos.cloud` API. Most "CORS error" / "502 on /api" reports are
> NGINX config, not the backend. Verify against the live `api.conf` before asserting a fix.

## NGINX is the CORS authority — not the services

CORS headers are set **once, in NGINX**, via the `cors_map.conf` + `cors_include.conf` pattern.
The allowed origin is chosen by a `map` on the request `Origin`.

- **Every new API `location` block must `include` the CORS snippet** — a location without it
  emits no CORS headers and the browser call fails.
- **A per-location `add_header` RESETS all inherited headers.** If you add any `add_header` inside
  a location, you must re-add the CORS headers there too, or CORS silently breaks for that path
  only. This is the #1 cause of "works on endpoint A, CORS-fails on endpoint B, same domain."
- Backends should **not** also emit `Access-Control-*` — double headers = browser rejects.

## Path prefixing — the double-prefix trap

External API paths are namespaced by service: `https://api.bcos.cloud/<service>/api/...`
(e.g. `/incident/api/...`, `/journal/api/...`). NGINX strips/rewrites the `<service>` segment
when proxying to the container.

- Some per-domain confs **prepend** a service prefix. If the frontend also sends the prefix you
  get a **double prefix** (`/devices/api/devices/api/...`) → 404. Location blocks like
  `/devices/api/`, `/fleet/api/`, `/transactions/api/` exist in `api.conf` to normalise this.
- **Journal agent block MUST come before the generic UI block.** Generic path-prepend rules
  otherwise double-prefix agent paths. Admin journal API external paths **drop** the
  `/api/journal/` segment — do not add it back.
- Order matters: **specific API `location`s first, SPA catch-all `location /` LAST.**

## Blue/Green backends

Seven+ services run blue/green. The active upstream is selected by a **`map` variable** in
`00-backends.conf` (e.g. `set $backend hiveiq-<svc>-blue:PORT`), not a hardcoded hostname.

- To switch slots: deploy to the **standby** color, verify, then flip the map var **and any
  internal callers** in sync — keep the old slot until verified.
- **Stale hardcoded hostnames = 502 on relative `/api/`.** Per-domain confs
  (devices/incident/dashboard/aria) once pinned pre-blue/green hostnames and 502'd; resolved
  2026-06-30 by moving them to the `map` vars. **If a domain 502s on `/api/` again, recheck this
  first.**

## Health-check noise

Several backends returned 401/403 to Uptime Kuma because the NGINX catch-all prefix injected a
path they auth-gate. Fix = an **exact-match `location =` passthrough** in `api.conf` for the
health path. A 401 on a health probe often means "NGINX reached the backend and it challenged,"
not "down."

## Do NOT
- Do not add CORS handling in a backend — NGINX owns it.
- Do not add an `add_header` in a location without re-adding CORS.
- Do not hardcode a blue/green hostname in a per-domain conf — use the `00-backends.conf` map var.
- Do not place an API `location` after `location /`.
