Skip to content

docs: ARC/DinD pattern — direct agent access to services: containers via the topology network #57988

Description

@mwpastore

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) 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 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.

# 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 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.<name>.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


🤖 Drafted with Claude Code; the pattern was designed and tested by a human, who reviewed and approved this issue before it was filed.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions