---
module: platform.deployment
title: Deployment Topology & bcos.dev
tab: Overview
order: 30
audience: dev
---

> **DRAFT · internal** (2026-07-01). Server paths verified today; re-check before prod actions.

## Environments
- **bcos.dev** — all-in-one dev/staging on VM **`173.231.195.251`**. Registry `registry.bcos.dev`
  (user `hiveiq`, pw in memory). Domains `*.bcos.dev`.
- **Production** — OpenMetal; services VM **`173.231.195.250`**, Postgres on a **separate** VM
  `10.10.10.188` (ProxyJump). Registry `registry.bcos.cloud`. Domains `*.bcos.cloud`.

## Where config lives vs. where deploys run (this bit me 2026-06-30)
- **Source of truth for bcos.dev config = the `develop` branch of `hiveiq-openmetal-dev`**
  (`/source/hiveiq-ops/hiveiq-openmetal/hiveiq-openmetal-dev/hiveops/instances/dev/`).
- The `dev` folder still inside `hiveiq-openmetal-prod` is **legacy — do not use it.**
- **Deploys RUN on `.251` from** `~/hiveiq/hiveops-openmetal/hiveops/instances/dev/` (a deployed
  clone named `hiveops-openmetal` — note `hiveops` on the box vs `hiveiq` in the source repo).
  That is the correct stack the running nginx + containers use.
- Live VM edits must be **committed back to `hiveiq-openmetal-dev`** or they drift.

## Deploy a service to bcos.dev

> ⛔ **CI builds the image. You do not.** Merging to `develop` runs `cd-develop.yml`
> (npm audit → **Trivy** → build → push `:dev` **and `:<sha8>`**). **Do NOT run `./build.sh`** —
> a hand-built image skips the security gate and races CI for the `:dev` tag. Full detail, the
> per-repo table of what CI actually builds, and the runner runbook are in **[[platform.ci-cd]]**.

**1. Merge to `develop` and let CI build.** Confirm the image exists before deploying:

```bash
curl -sk -u hiveiq:<pw> https://registry.bcos.dev/v2/hiveiq-<svc>/tags/list   # expect your :<sha8>
```

**2. Deploy — but check whether CI already did it.** `cd-develop` **only deploys for
`hiveops-incident` and `hiveops-devices`.** For every other repo CI stops at push, and you deploy:

```bash
ssh -i ~/.ssh/id_ed25519_hiveiq bcosadmin@173.231.195.251
cd ~/hiveiq/hiveops-openmetal/hiveops/instances/dev
# pin the immutable, scanned tag in .env — never deploy from :dev or :latest
docker compose pull hiveiq-<svc> && docker compose up -d hiveiq-<svc>
```

**Then smoke-test it** (see the bottom of this page). A green container is not a deploy.

## Production blue/green cutover (services VM)
Six prod services run blue/green (auth, incident, devices, fleet, transactions, agent-proxy). The
routing mechanics — the `00-backends.conf` map, the targeted-flip gotcha, and how **internal
service-to-service calls now route through `hiveiq-nginx`** — live in **platform.nginx-routing**.

- **Deploy to the STANDBY color only, then flip the map** — use `scripts/bluegreen-deploy.sh <svc>
  <ver>`. **Never `docker compose up -d` the ACTIVE color** — that recreates the live container =
  a full outage, not a cutover.
- **2026-07-03 incident:** a hand-deploy recreated the active `incident` container → ~35-min
  fleet-wide agent-ingestion outage (11.7k dropped journal-event POSTs), because internal callers
  hardwired the color and bypassed nginx. Fixed by routing internal calls through nginx
  (`10-internal-backends.conf`, PR hiveiq-ops/hiveiq-openmetal-prod#20). Now one map flip + reload
  cuts over both external and internal traffic. See hiveiq-ops/hiveiq-devops#27.

## nginx routing gotchas
- API routes live in `.../instances/dev/nginx/conf.d/api.conf` (bind-mounted into `hiveiq-nginx`).
- Backends are set per-location: `set $backend "hiveiq-<svc>:<port>"` then `proxy_pass`. **Include
  the port** — a port-less upstream (`hiveiq-guide-backend` with no `:8099`) proxies to :80 → 502.
  (Exact bug, 2026-07-01.)
- After recreating a container, nginx may hold a **stale upstream** → reload: `docker exec
  hiveiq-nginx nginx -t && docker exec hiveiq-nginx nginx -s reload`.
- Per-domain confs must use blue/green **map vars**, not hardcoded hostnames, or they 502.

## Deploy = smoke test
"Deployed" is only true after a curl of the changed endpoint against the live URL returns 200 +
a parseable body. A green/healthy container is not a smoke test.
