docs: ARC/DinD pattern — direct agent access to services: containers via the topology network
- Dominant language
- Go
- Stars
- 5.1k
- Forks
- 541
- Avg merge
- 5h 46m
- Merged PRs (30d)
- 760
Description
## Use case
On `runner.topology: arc-dind`, a workflow's `services:` containers are unreachable from the sandboxed agent, so any workload that must speak a service's native protocol — an integration-test suite dialing a database driver, a migration tool, a client library under test — cannot run inside the sandbox. An MCP server (per [guides/mcps.md](https://github.github.com/gh-aw/guides/mcps/)) covers the case where the *agent* queries the service, but not the case where the *code under test* is the client.
There is a small, capability-free pattern that solves this on ARC/DinD, verified end to end. This issue proposes documenting it (or productizing it — see Implementation options). Postgres is the worked example; the pattern applies to any TCP service container.
## Why the documented routes don't apply on ARC/DinD
All measured on gh-aw v0.87.10 / AWF v0.28.10, ARC `gha-runner-scale-set` 0.14.2 in dind mode:
- The runner places `services:` containers on its own `github_network_` bridge; AWF places the agent on the internal `awf-net`. Two unrouted bridges on the same DinD daemon.
- Host-service-port routing (#51433 → #51842) works in strict mode on VM runners, but on ARC/DinD it depends on host iptables that network-isolation mode never programs (gh-aw-firewall#7266, closed *not planned*). Observed: the `host.docker.internal` alias is attached only to the squid container, squid gets no ACL for the service port, and the `awf-net` gateway refuses connections.
- `--enable-host-access` is accepted but has no agent-visible effect under isolation; `--no-network-isolation` would strand the MCP gateway (the compiler bakes `MCP_GATEWAY_DOMAIN="awmg-mcpg"`, a name that exists only on the isolated network).
## The pattern
The agent and the service are containers on the same DinD daemon; they need a shared network and a name, not a route through the host. `docker network connect` adds a second interface to the running service container on `awf-net` — the same mechanism AWF itself uses for topology peers — and Docker's embedded DNS (the agent's resolver) serves the alias.
```yaml
# 1. Declare the service with NO published ports. Ports are unnecessary (the
# agent reaches it over awf-net) and omitting them avoids the published-ports
# check, so the default `docker` runtime profile suffices — no
# docker-sudo-iptables, no sudo, no extra capabilities.
services:
postgres:
image: postgres:18.3@sha256:...
env:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app
options: >-
--health-cmd "pg_isready --username=app --dbname=app"
--health-interval 5s --health-timeout 5s --health-retries 24
# 2. Point the workload at the alias.
engine:
env:
ConnectionStrings__app: Host=service-db;Port=5432;Database=app;Username=app;Password=app
# 3. A pre-step launches a background waiter. awf-net is created by AWF inside
# the agent step — after pre-steps have run — so the waiter polls for it,
# then joins the service container with a stable alias.
steps:
- name: Join the service container to awf-net when it appears
run: |
for i in $(seq 1 60); do
SVC=$(docker ps --format '{{.Names}}' | grep -i postgres | head -1)
[ -n "$SVC" ] && break
sleep 2
done
if [ -z "$SVC" ]; then echo "::error::service container not found"; exit 1; fi
nohup bash -c '
for i in $(seq 1 600); do
docker network inspect awf-net >/dev/null 2>&1 && break
sleep 2
done
docker network connect --alias service-db awf-net "'"$SVC"'"' \
> "${RUNNER_TEMP}/svc-join.log" 2>&1 &
disown
```
The pre-step's `docker` CLI drives the DinD daemon over the shared socket; the daemon (already privileged in ARC dind mode) performs the namespace work, so the runner container needs no added capabilities.
## Security considerations
- **Join direction is load-bearing.** The service joins the agent's *internal* network and gains no egress from it. Never attach the agent to the runner's network — that would bypass the egress firewall.
- The service container keeps its original runner-network interface (with egress), unlike a pure `--topology-attach` peer. Join only trusted images — the same trust already extended to `services:` containers generally.
- Raw TCP between `awf-net` peers does not traverse squid; the egress allowlist is unaffected.
## Verified
- Manual join on a running container, then automated (pre-step) join: agent resolves the alias via embedded DNS, TCP opens, and the service answers protocol-level probes (Postgres replied `N` to an SSLRequest).
- Full end-to-end workload: `dotnet test` integration suite over Npgsql through an env-var connection string — 669/669 passed, 0 skipped, on a stock-capability runner pod under the default `docker` runtime profile.
- Evidence lives in a private repository; happy to share log excerpts on request.
## Implementation options (maintainers' pick)
1. **Docs only**: a section under [reference/self-hosted-runners](https://github.github.com/gh-aw/reference/self-hosted-runners/) or the ARC/DinD guide, with the snippet above. Positioned relative to guides/mcps.md: MCP server when the agent is the client; this pattern when the workload is.
2. **Productize**: compiler sugar (e.g. `services..attach: true` or extending `network.topologyAttach` to service containers) emitting the waiter automatically — the pattern reduces to one frontmatter key and removes the shell from user space. The doc then shrinks to the key and the security note.
## Related work
- #22939, #51433, #52140 — `services:` unreachable from the sandbox (the ARC/DinD case remains, per gh-aw-firewall#7266 *not planned*).
- #51842 — fixed the VM-runner case via `--allow-host-ports` in strict mode; this proposal covers the topology that fix cannot reach.
- #40107, #44561, #46045 — the ARC/DinD guide this could extend.
---
🤖 Drafted with [Claude Code](https://claude.com/claude-code); the pattern was designed and tested by a human, who reviewed and approved this issue before it was filed.
Contributor guide
Research direction
Start with reference/self-hosted-runners or the ARC/DinD guide, and compare the proposed positioning with guides/mcps.md and related issues #40107, #44561, and #46045. Use the verified service-join example and security considerations as the documentation acceptance criteria; done means the ARC/DinD limitation, safe join direction, and workload-versus-MCP distinction are clearly covered.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- docker, github-actions, postgresql
- Domain
- devops, documentation, infrastructure
- Issue type
- Documentation
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100