Key points
- Compose creates one bridge network named
nopsai-net; every service resolves the others by container name on it. - Five ports are published to the host, all on
127.0.0.1by default: UI 80, API 8080, git-bot 8081, dispatcher 9091, PostgreSQL 5432. - In-network addresses are fixed by Compose: the API is
http://nopsai:8080, AAA ishttp://aaa:8082, git-bot ishttp://nopsai-git-bot:8081, Gotenberg ishttp://gotenberg:3000, and the Docker socket proxy istcp://docker-socket-proxy:2375. - The dispatcher process listens on
:9090inside its container; the published host port 9091 maps onto it. Callers find it throughDISPATCHER_GRPC_ADDRESS. - Runners dial out to the dispatcher and keep that stream open. Work is assigned back over it, so a runner needs no inbound port and no public address.
- The
docker-runnerservice in the checked-in Compose file only builds the image — its entrypoint istrue. A working runner is installed separately.
How it works
Two planes share the network. The durable control plane is the API, AAA, PostgreSQL, git-bot, Gotenberg, and the socket proxy: everything that must survive a restart lives there. The ephemeral execution plane is the dispatcher, runners, per-run agents, and step containers.
Only two hops need to work from a runner host: the dispatcher gRPC endpoint it dials, and the API URL the per-run agent calls back on for status, logs, outputs, and approvals. Everything else the runner needs arrives over those two connections.
The Docker socket proxy exists so System Logs can read container logs without handing the API a writable Docker socket. It exposes allow-listed reads only.
When the generated runner install detects that the dispatcher address resolves to the machine running the command, it rewrites the address to host.docker.internal and adds a host-gateway mapping, because a bridge container cannot reach the host through localhost.
Inbound Git traffic terminates at git-bot on 8081 for GitHub App deliveries, while other providers deliver to the API through a configured Git webhook source. Nothing else needs to be reachable from the internet.
Examples
host (127.0.0.1) nopsai-net (bridge)
:80 ───────────────▶ nopsai-ui
│ http://nopsai:8080
:8080 ───────────────▶ nopsai ──────┬───────▶ aaa (http://aaa:8082)
│ ├───────▶ db (postgres:5432)
│ ├───────▶ gotenberg (http://gotenberg:3000)
│ └───────▶ docker-socket-proxy (tcp://…:2375)
│
:8081 ───────────────▶ nopsai-git-bot ──────▶ nopsai
│
:9091 ───────────────▶ dispatcher (listens :9090)
▲
│ runner dials out and keeps the stream open
docker runner ──▶ agent ──▶ step containers
:5432 ───────────────▶ dbdocker network inspect nopsai-net --format '{{range .Containers}}{{.Name}} {{.IPv4Address}}\n{{end}}'curl -s localhost:8080/livez
curl -s localhost:8080/healthz
curl -s localhost:8080/version
curl -sI localhost/ | head -1Implementation evidence
docker-compose.yamlNetwork name, published ports, and the in-network service addresses.
services/dispatcher/internal/app/app.goDispatcher listen address default.
services/nopsai/internal/runnerinstall/docker.goHost-gateway rewrite and runner network placement.

