Files
channel-exit/docker-compose.yml
T
Nirav PatelandClaude Opus 5 399487862c
CI / test (push) Successful in 16s
Reach consumers by network, not by a second host bind
The compose file published a second host port on the docker bridge IP so
sibling containers could reach the relay. That is a host-reachable address for
a service whose entire safety story is that it has exactly one, on loopback.

It joins the consumers' existing docker network instead, and is reached there
by name on the container port. Nothing about a consumer's deployment has to
change to use it, and the relay gains no address outside that network.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-18 19:25:11 -04:00

59 lines
2.6 KiB
YAML

# How this runs on the host. There is one service and no database: the relay
# keeps nothing, which is deliberate — an outbox of everyone's reminders is a
# liability, and the providers already have delivery logs.
#
# The bind address below is the security boundary of this whole service. The
# only host port it publishes is on 127.0.0.1 — for a human on the host and for
# host-side consumers. It must never be given a 0.0.0.0 binding, a bridge-IP
# binding, or a cloudflared route. An authenticated relay on the public
# internet is an open spam relay the moment the token leaks, and that token is
# copied into every consumer's environment.
#
# Containerised consumers reach it a different way, and this is the part that
# is easy to get wrong: `127.0.0.1` inside a consumer's container is that
# container, so a consumer pointed at a loopback URL fails every delivery while
# looking correctly configured. Instead this joins the docker network its
# consumers already live on and is reached there by name, on the container
# port:
#
# from the host http://127.0.0.1:${CHANNEL_EXIT_PORT}/send
# from a sibling http://channel-exit:8080/send
#
# That is strictly tighter than publishing on the docker bridge IP: it adds no
# host-reachable address at all, and only containers on that network can see it.
services:
channel-exit:
image: ${CHANNEL_EXIT_IMAGE:-channel-exit:local}
build: .
restart: unless-stopped
env_file: .env
ports:
- "127.0.0.1:${CHANNEL_EXIT_PORT:?allocate a port in the 5400-5999 range}:8080"
networks:
consumers:
aliases: [channel-exit]
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8080/healthz', timeout=4).status == 200 else 1)"]
interval: 15s
timeout: 5s
start_period: 10s
retries: 3
logging:
# Bounded, because this log names who was messaged and when. It should
# not accumulate on disk indefinitely.
driver: json-file
options:
max-size: "10m"
max-file: "3"
networks:
# Created by whoever owns the consumers, not by this compose file — the relay
# joins their network rather than asking them onto one of its own, so nothing
# about their deployment has to change to start using it. `external` is what
# makes that true: if the network is missing, this fails to start instead of
# silently creating an empty one nobody else is on.
consumers:
external: true
name: ${CHANNEL_EXIT_NETWORK:?the existing docker network the consumers are on}