A peer that never closes after receiving the relayed FIN used to pin the forward's goroutine pair and both TCP connections until SIGKILL — and with them the drain in main. Force-closing both ends as soon as one direction finishes would have thrown out TCP half-close (a client that FINs its request and then reads a long response), so the surviving direction now runs under an idle grace instead: a read deadline re-armed before every Read once the other direction is done. Active streams are never cut — only pairs sitting idle past the grace are reaped. Locked by two tests: the silent-peer pair is reaped at the grace, and a response still streaming after the client's half-close survives well past it. The demo provider's comment also spells out why serve-demo is neither Wait()ed nor a zombie: the provider exits within seconds, so init has long adopted — and reaps — the subprocess when its three minutes are up. Co-Authored-By: Claude Fable 5 <[email protected]> Signed-off-by: Nicolas De Loof <[email protected]>
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 thecom.docker.compose.relaylabel identifying its role. Its value is a hash of the routes, lettingupkeep 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