# 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}