compose/relay/README.md
Nicolas De Loof aab5819147 feat(provider): publish-endpoint deploys a network relay for provider services
A provider's resource lives outside the compose network: consumers could
only reach it through injected variables carrying a host-published
address — nothing like the compose-native experience of addressing a
service by name at its well-known port.

A provider may now publish where each endpoint of its resource actually
listens:

    {"type": "publish-endpoint", "message": "80=localhost:49152"}

The endpoint is announced as seen from the provider's host: the relay —
the component that knows it runs inside a container — rewrites loopback
or unspecified upstream hosts to host.docker.internal (resolved through
its injected host-gateway extra_host); routable addresses pass through.

When at least one endpoint is published, compose deploys a relay
container in place of the service: a minimal TCP forwarder (new relay/
directory, published as docker/compose-relay, overridable with
COMPOSE_RELAY_IMAGE for internal registries) joining the networks of the
services that depend on the provider service, aliased with the service
name. Consumers then use http://<service>:<port> as if the service were
a regular container.

The relay is a first-class project container — canonical name, standard
compose labels including config-hash (label-driven commands run without
the compose file keep seeing the service: ps, logs, stop, down) — plus
the com.docker.compose.relay label declaring its role:

- the reconciler already leaves provider services' containers alone, and
  the relay's identity hash (image + routes) makes up idempotent: kept
  when routes are unchanged, recreated otherwise;
- process-level commands (exec, cp) refuse a relay — there is no service
  process in it to act on;
- the up monitor excludes relays from the containers whose termination
  ends an attached up: they are long-lived infrastructure and would
  otherwise keep 'up' waiting forever.

The example provider demonstrates the flow behind PROVIDER_DEMO_ENDPOINT
(a detached helper serving a fixed HTTP response), backed by an e2e
scenario asserting the compose-native address works and exec is refused.

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2026-09-16 17:03:23 +02:00

2.6 KiB

compose-relay

compose-relay is the network gateway Docker Compose deploys in place of a provider-managed service, so the other services of the project reach the provider's resource at the compose-native address — http://<service>:<port> — even though that resource lives outside the compose network.

Why it exists

A service can delegate its implementation to an external provider:

services:
  web:
    build: .
    depends_on:
      - database

  database:
    provider:
      type: awesomecloud

The provider's resource (a cloud database, a sandbox, a host process, ...) is not a container on the project network: web cannot resolve database, and the resource's ports are typically published somewhere on the host, at addresses and port numbers the application does not know. Until now consumers had to read injected environment variables to locate it.

When the provider declares where each endpoint actually listens, with one publish-endpoint message per port:

{ "type": "publish-endpoint", "message": "5432=localhost:49152" }

Compose deploys this relay in place of the service. web then connects to database:5432 exactly as if the service were a regular container; the relay forwards the connection to the real endpoint.

How it runs

Compose creates the relay container from the published docker/compose-relay image (override with COMPOSE_RELAY_IMAGE, e.g. for air-gapped setups or local development) with:

  • the service's canonical container name (<project>-<service>-1) and a network alias set to the service name, on the networks of every service that depends on the provider service;

  • the standard compose labels, so label-driven commands (ps, logs, stop, down) treat it as the service — plus the com.docker.compose.relay label identifying its role. Its value is a hash of the routes, letting up keep an up-to-date relay and recreate a stale one. Commands that act on a service's process (exec, cp) refuse relay containers;

  • the routes as environment:

    RELAY_ROUTES=5432=localhost:49152[,<port>=<host>:<port>...]
    

The binary listens on every declared container port and forwards each connection to its endpoint. TCP only, with half-close propagation so protocols relying on EOF work through the relay. It is intentionally minimal: a static Go binary on a scratch image, no configuration reload — Compose recreates the relay when the published endpoints change.

Building

$ docker buildx bake relay-image