152 lines
6.0 KiB
YAML
152 lines
6.0 KiB
YAML
# Flamenet relay, FEDERATED: your own server that exchanges mail with other
|
|
# relays, including relay.flamenet.io, while your accounts and mailboxes
|
|
# stay on your hardware.
|
|
#
|
|
# 1. cp .env.example .env and fill in the secrets it asks for,
|
|
# INCLUDING the "Federation" and "Calls (TURN)" sections
|
|
# 2. point DNS for $FN_PUBLIC_HOST at this machine (A/AAAA, no proxy CDN,
|
|
# TURN and long-polls both dislike middleboxes)
|
|
# 3. docker compose -f docker-compose.federated.yml up -d
|
|
# 4. open https://$FN_PUBLIC_HOST, the relay serves its own landing page
|
|
#
|
|
# What this adds over the plain docker-compose.yml:
|
|
# caddy , TLS on 443, automatic certificates. Federation peers dial your
|
|
# public hostname over HTTPS; a relay on 127.0.0.1:8080 federates
|
|
# with nobody.
|
|
# redis , rate limits and replay protection that survive a restart,
|
|
# and wake-ups for held polls when FN_RELAY_REPLICAS is above 1.
|
|
# One relay wakes held polls in memory.
|
|
# coturn , voice/video call relaying for peers behind NAT. Without it,
|
|
# calls connect only when both phones can reach each other
|
|
# directly, which across the public internet is "sometimes".
|
|
#
|
|
# What federation CANNOT give a self-hoster: iOS background push. Apple
|
|
# ties push credentials to the app's own developer team, so only the relay
|
|
# the shipped app was built for can wake a closed iPhone. On your relay, iOS
|
|
# receives when the app is open or foregrounded. Android's foreground
|
|
# service is unaffected and alerts in about a second either way. That is a
|
|
# platform constraint, stated here so you hear it from us and not from a
|
|
# confused tester.
|
|
|
|
services:
|
|
db:
|
|
image: postgres:17-alpine
|
|
restart: unless-stopped
|
|
environment:
|
|
POSTGRES_DB: flamenet
|
|
POSTGRES_USER: flamenet
|
|
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}"
|
|
volumes:
|
|
- db-data:/var/lib/postgresql/data
|
|
# Reached over the compose network only. An exposed database is the kind
|
|
# of default that ends up on Shodan.
|
|
expose:
|
|
- "5432"
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U flamenet -d flamenet"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 10
|
|
|
|
redis:
|
|
image: redis:7-alpine
|
|
restart: unless-stopped
|
|
command: ["redis-server", "--save", "", "--appendonly", "no"]
|
|
# Wake-up pub/sub only, nothing here needs to survive a restart, and a
|
|
# persistence file would be one more thing holding delivery metadata.
|
|
expose:
|
|
- "6379"
|
|
|
|
relay:
|
|
build: .
|
|
image: flamenet-relay:federated
|
|
restart: unless-stopped
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
environment:
|
|
FN_HOST: "0.0.0.0"
|
|
FN_LOG_LEVEL: "${FN_LOG_LEVEL:-error}"
|
|
FN_POSTGRES_URL: "postgres://flamenet:${POSTGRES_PASSWORD}@db:5432/flamenet"
|
|
FN_REDIS_URL: "redis://redis:6379"
|
|
# The audience IS your public hostname on a federated relay. Peers
|
|
# verify you by it, your users' addresses end in it, and tokens are
|
|
# bound to it, an audience that doesn't match the name peers dial is
|
|
# a relay that authenticates with nobody.
|
|
FN_RELAY_AUDIENCE: "${FN_PUBLIC_HOST:?set FN_PUBLIC_HOST in .env}"
|
|
FN_ISSUER_PUBLIC_KEY: "${FN_ISSUER_PUBLIC_KEY:?generate an issuer keypair - see .env.example}"
|
|
FN_ISSUER_PRIVATE_KEY: "${FN_ISSUER_PRIVATE_KEY:?generate an issuer keypair - see .env.example}"
|
|
# Federation proper, see the Federation section of .env.example.
|
|
FN_FEDERATION: "${FN_FEDERATION:-allowlist}"
|
|
FN_FEDERATION_PEERS: "${FN_FEDERATION_PEERS:-}"
|
|
FN_FEDERATION_KEY: "${FN_FEDERATION_KEY:?set FN_FEDERATION_KEY - see .env.example}"
|
|
# Calls. The secret must equal coturn's static-auth-secret below, the
|
|
# relay mints short-lived TURN credentials from it (§8.10).
|
|
FN_TURN_SECRET: "${FN_TURN_SECRET:?set FN_TURN_SECRET in .env}"
|
|
FN_TURN_HOST: "${FN_PUBLIC_HOST}"
|
|
# Ciphertext is deleted the moment the recipient acknowledges it.
|
|
# Postgres-only behaviour, and this profile is Postgres-only.
|
|
FN_DELETE_ON_ACK: "true"
|
|
# Everything else `.env` documents reaches the relay through the file
|
|
# itself. What is listed above wins over it.
|
|
env_file:
|
|
- .env
|
|
expose:
|
|
- "8080"
|
|
volumes:
|
|
- attachments:/data/attachments
|
|
|
|
caddy:
|
|
image: caddy:2-alpine
|
|
restart: unless-stopped
|
|
ports:
|
|
- "80:80"
|
|
- "443:443"
|
|
# The whole reverse-proxy config is this one command: terminate TLS for
|
|
# the public hostname, hand everything to the relay. Certificates come
|
|
# from Let's Encrypt automatically; port 80 stays open for the challenge.
|
|
command: >
|
|
caddy reverse-proxy
|
|
--from ${FN_PUBLIC_HOST:?set FN_PUBLIC_HOST in .env}
|
|
--to relay:8080
|
|
volumes:
|
|
- caddy-data:/data
|
|
|
|
coturn:
|
|
image: coturn/coturn
|
|
restart: unless-stopped
|
|
# Host networking: TURN allocates relay ports dynamically, and publishing
|
|
# a 40-port range through the Docker proxy costs a process per port.
|
|
network_mode: host
|
|
# Client IP:port reaches this log. One stream and a size cap, rather
|
|
# than dated files inside the container that nothing ever deletes.
|
|
logging:
|
|
driver: json-file
|
|
options:
|
|
max-size: "1m"
|
|
max-file: "2"
|
|
command:
|
|
- "--no-cli"
|
|
- "--log-file=stdout"
|
|
- "--realm=${FN_PUBLIC_HOST}"
|
|
- "--fingerprint"
|
|
- "--use-auth-secret"
|
|
- "--static-auth-secret=${FN_TURN_SECRET}"
|
|
- "--listening-port=3478"
|
|
- "--listening-ip=0.0.0.0"
|
|
- "--min-port=49160"
|
|
- "--max-port=49200"
|
|
# Set this to the machine's PUBLIC address in .env. Behind NAT,
|
|
# coturn must know the address the outside world sees or every
|
|
# allocation hands out an unreachable candidate.
|
|
- "--external-ip=${FN_TURN_EXTERNAL_IP:?set FN_TURN_EXTERNAL_IP in .env}"
|
|
- "--no-tls"
|
|
- "--no-dtls"
|
|
- "--no-multicast-peers"
|
|
- "--no-rfc5780"
|
|
|
|
volumes:
|
|
db-data:
|
|
attachments:
|
|
caddy-data:
|