weavesignal/deploy/compose/compose.yaml
The WeaveSignal contributors 466b32ab6a WeaveSignal 1.0.0 — release one
The first public release. What it does: https://weavesignal.top; what changed: CHANGELOG.md.
2026-11-04 12:15:42 +00:00

375 lines
16 KiB
YAML

# SPDX-License-Identifier: AGPL-3.0-only
# SPDX-FileCopyrightText: 2026 The WeaveSignal contributors
# WeaveSignal's M1 stack on one machine, every listener on loopback: LiveKit, the desk, playout
# and the listener process (Liquidsoap 2.5, two processes: playout feeds the listener process,
# which holds the mount and its fallback), the control plane, the chat and PostgreSQL. It is
# the loopback stack the tests run, as a bundle. The public route (a reverse proxy with
# certificates, TURN over TLS) is the opt-in overlay public.yaml, which `weavesignal.sh --public
# <domain>` turns on (ADR-0022).
#
# cp .env.example .env # then fill in every secret
# podman build -t localhost/weavesignal-server:m1 -f ../images/server/Containerfile ../..
# podman compose up -d # or docker compose
#
# Every port is a setting (ADR-0075): each one below comes from .env. Host networking keeps
# the loopback binds exact; PostgreSQL listens on a Unix socket only, so it takes no port.
name: weavesignal
x-liquidsoap: &liquidsoap
image: docker.io/savonet/liquidsoap:v2.5.0-rc1
network_mode: host
# The scripts are bind-mounted from the repository; on SELinux hosts the container would
# otherwise be refused them.
security_opt: [label=disable]
entrypoint: ["/usr/bin/liquidsoap"]
environment:
WEAVESIGNAL_PLAYOUT_DATA: /etc/weavesignal/playout.json
WEAVESIGNAL_LISTENERS_SOURCE_PASSWORD: ${LISTENERS_SOURCE_PASSWORD:?set LISTENERS_SOURCE_PASSWORD in .env}
volumes:
- ../../playout:/opt/weavesignal/playout:ro
configs:
- source: playout
target: /etc/weavesignal/playout.json
restart: unless-stopped
services:
livekit:
image: docker.io/livekit/livekit-server:v1.13.10
network_mode: host
command: ["--config", "/etc/livekit.yaml"]
environment:
LIVEKIT_KEYS: "${LIVEKIT_API_KEY:?set LIVEKIT_API_KEY in .env}: ${LIVEKIT_API_SECRET:?set LIVEKIT_API_SECRET in .env}"
configs:
- source: livekit
target: /etc/livekit.yaml
restart: unless-stopped
listeners:
<<: *liquidsoap
command: ["/opt/weavesignal/playout/listeners.liq"]
# The control plane's user: its socket (0600) is the control plane's to open, and the
# logger's files are the control plane's to delete when retention says so.
user: "999:999"
volumes:
- ../../playout:/opt/weavesignal/playout:ro
- listeners-control:/run/weavesignal-listeners
- recordings:/var/lib/weavesignal/recordings
- fallback-media:/var/lib/weavesignal/fallback:ro
# The default loop, played until the station adds fallback files of its own (ADR-0130).
- ../../assets/default-loop:/opt/weavesignal/assets/default-loop:ro
depends_on: [control]
playout:
<<: *liquidsoap
command: ["/opt/weavesignal/playout/weavesignal.liq"]
# The control plane's user, so its control socket (0600) is the control plane's to open;
# the volume takes its owner from the server image, so the control plane is made first.
user: "999:999"
volumes:
- ../../playout:/opt/weavesignal/playout:ro
- playout-control:/run/weavesignal-playout
- library-media:/var/lib/weavesignal/library:ro
- fallback-media:/var/lib/weavesignal/fallback:ro
- ../../assets/default-loop:/opt/weavesignal/assets/default-loop:ro
# The wall's carts sound in the desk; a clock's scheduled carts (idents) play here.
- carts-media:/var/lib/weavesignal/carts:ro
depends_on: [listeners, control]
desk:
image: ${SERVER_IMAGE:-forgejo.weavesignal.top/weavesignal/weavesignal-server:1.0.0}
network_mode: host
command: ["weavesignal-desk", "--config", "/etc/weavesignal/desk.toml"]
environment:
WEAVESIGNAL_LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET}
# The password playout sends with its feed back to the desk (video on, ADR-0172).
WEAVESIGNAL_PLAYOUT_FEED_PASSWORD: ${LISTENERS_SOURCE_PASSWORD}
RUST_LOG: ${RUST_LOG:-info}
configs:
- source: desk
target: /etc/weavesignal/desk.toml
volumes:
- desk-control:/run/weavesignal
# The cart files the media worker wrote: the desk decodes and plays them (ADR-0117).
- carts-media:/var/lib/weavesignal/carts:ro
# The CMAF HLS the desk writes with video on; the control plane serves it at /hls/.
- hls-media:/var/lib/weavesignal/hls
depends_on: [livekit]
# The desk stops on a fatal pipeline error and is started again here; playout connects
# to it again within about a second (`just test-playout`).
restart: unless-stopped
postgres:
image: docker.io/library/postgres:18
network_mode: host
command: ["postgres", "-c", "listen_addresses="]
environment:
POSTGRES_USER: weavesignal
POSTGRES_DB: weavesignal
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
volumes:
- postgres-data:/var/lib/postgresql
- postgres-socket:/var/run/postgresql
# Healthy once the station's database answers: during first start the image runs a
# temporary server on the same socket before the database exists.
healthcheck:
test: ["CMD", "psql", "-U", "weavesignal", "-d", "weavesignal", "-c", "SELECT 1"]
interval: 2s
timeout: 5s
retries: 30
start_period: 5s
restart: unless-stopped
control:
image: ${SERVER_IMAGE:-forgejo.weavesignal.top/weavesignal/weavesignal-server:1.0.0}
network_mode: host
command: ["weavesignal-control", "--config", "/etc/weavesignal/control.toml", "serve"]
environment:
WEAVESIGNAL_DATABASE_URL: postgres://weavesignal@localhost/weavesignal?host=/var/run/postgresql
WEAVESIGNAL_LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET}
WEAVESIGNAL_CONTROL_ADMIN_TOKEN: ${CONTROL_ADMIN_TOKEN:-}
# Keys the hash of staff email addresses and sign-in codes (ADR-0127); with MAIL_MODE on,
# the control plane will not start without it (weavesignal.sh makes one).
WEAVESIGNAL_CONTROL_EMAIL_KEY: ${CONTROL_EMAIL_KEY:-}
WEAVESIGNAL_CONTROL_TOKEN_KEY: ${CONTROL_TOKEN_KEY:-}
configs:
- source: control
target: /etc/weavesignal/control.toml
volumes:
- postgres-socket:/var/run/postgresql
# The desk's control socket, so the studio app drives the desk through the control plane.
- desk-control:/run/weavesignal
- playout-control:/run/weavesignal-playout
- listeners-control:/run/weavesignal-listeners
- carts-media:/var/lib/weavesignal/carts
- library-media:/var/lib/weavesignal/library
- recordings:/var/lib/weavesignal/recordings
- hls-media:/var/lib/weavesignal/hls:ro
- show-cards:/var/lib/weavesignal/show-cards
- episodes-media:/var/lib/weavesignal/episodes
depends_on:
desk:
condition: service_started
postgres:
condition: service_healthy
restart: unless-stopped
# The live chat (M6, ADR-0031): its own process on the station's database, following the
# control plane's studio view for the room's modes. Sign-in codes go nowhere until the
# station sets [mail] (MAIL_MODE).
chat:
image: ${SERVER_IMAGE:-forgejo.weavesignal.top/weavesignal/weavesignal-server:1.0.0}
network_mode: host
command: ["weavesignal-chat", "--config", "/etc/weavesignal/chat.toml", "serve"]
environment:
WEAVESIGNAL_DATABASE_URL: postgres://weavesignal@localhost/weavesignal?host=/var/run/postgresql
WEAVESIGNAL_CONTROL_ADMIN_TOKEN: ${CONTROL_ADMIN_TOKEN:-}
# Keys the hash of chatters' email addresses (ADR-0125). The chat refuses to start without
# it (at least 32 characters), so an empty value stops the chat with that said in its log.
WEAVESIGNAL_CHAT_EMAIL_KEY: ${CHAT_EMAIL_KEY:?set CHAT_EMAIL_KEY in .env (weavesignal.sh makes one)}
configs:
- source: chat
target: /etc/weavesignal/chat.toml
volumes:
- postgres-socket:/var/run/postgresql
depends_on:
control:
condition: service_started
postgres:
condition: service_healthy
restart: unless-stopped
volumes:
postgres-data:
postgres-socket:
desk-control:
# Playout's control socket.
playout-control:
# The cart files the media worker wrote: the control plane writes, the desk reads.
carts-media:
# The listener process's socket: the control plane turns the legal logger on and off.
listeners-control:
# The music library (the media worker's FLAC): the control plane writes, playout reads.
library-media:
# The station's fallback files, played while playout or the desk is away.
fallback-media:
# Recordings retention keeps and sweeps: the listener process writes the logger's hours.
recordings:
# The desk's CMAF HLS (video on): the desk writes, the control plane serves.
hls-media:
# Show cards the control plane draws, and the published episode files: served at /cards/ and
# /media/episodes/ while the station has a public address (PUBLIC_ORIGIN, the public route).
show-cards:
episodes-media:
configs:
livekit:
content: |
# Loopback and one UDP media port (ADR-0023). In this file's own defaults ICE over TCP and
# TURN are off; the public route (public.yaml, weavesignal.sh --public) turns them on through
# the settings named here. A setting left unset keeps the loopback value.
port: ${LIVEKIT_HTTP_PORT:-7880}
bind_addresses: ["127.0.0.1"]
rtc:
udp_port: ${LIVEKIT_MEDIA_PORT:-7882}
tcp_port: ${LIVEKIT_TCP_PORT:-0}
use_external_ip: ${LIVEKIT_USE_EXTERNAL_IP:-false}
node_ip: ${LIVEKIT_NODE_IP-127.0.0.1}
# TURN over TLS (ADR-0059, ADR-0080) with the proxy's certificate for the station's name,
# read from the proxy's data volume (public.yaml mounts it at /caddy-data). LiveKit reads
# the certificate once, when it starts. LiveKit's TURN refuses relay to private and loopback
# addresses, its own included, unless listed in allow_restricted_peer_cidrs (a node_ip
# behind NAT needs its address there).
turn:
enabled: ${TURN_ENABLED:-false}
domain: "${PUBLIC_DOMAIN:-}"
tls_port: ${TURN_TLS_PORT:-5349}
udp_port: ${TURN_UDP_PORT:-0}
external_tls: false
cert_file: /caddy-data/caddy/certificates/${TURN_CERT_DIR:-local}/${PUBLIC_DOMAIN:-none}/${PUBLIC_DOMAIN:-none}.crt
key_file: /caddy-data/caddy/certificates/${TURN_CERT_DIR:-local}/${PUBLIC_DOMAIN:-none}/${PUBLIC_DOMAIN:-none}.key
allow_restricted_peer_cidrs: [${TURN_ALLOW_CIDRS:-}]
# What the desk decodes and the pages publish (0004 §4): no H.264 or H.265.
room:
enabled_codecs:
- mime: audio/opus
- mime: audio/red
- mime: video/vp8
- mime: video/vp9
- mime: video/av1
logging:
level: info
playout:
content: |
{
"station_id": "${STATION_ID:?set STATION_ID in .env}",
"srt": { "host": "127.0.0.1", "port": ${SRT_INGEST_PORT:-7313} },
"listeners": { "bind": "127.0.0.1", "host": "127.0.0.1", "port": ${LISTENERS_PORT:-7312}, "mount": "programme.opus", "feed_port": ${PLAYOUT_FEED_PORT:-7315}, "feed_buffer_s": ${FEED_BUFFER_S:-0.5}, "directory": ${DIRECTORY_LISTING:-false}, "compat": { "enabled": ${COMPAT_STREAM:-false}, "mount": "${COMPAT_MOUNT:-programme.mp3}", "bitrate": ${COMPAT_BITRATE:-128} }, "control_socket": "/run/weavesignal-listeners/listeners.sock" },
"desk_feed": { "host": "127.0.0.1", "port": ${DESK_FEED_PORT:-7317}, "enabled": ${VIDEO:-false} },
"control_socket": "/run/weavesignal-playout/playout.sock",
"carts": { "dir": "/var/lib/weavesignal/carts" },
"library": { "dir": "/var/lib/weavesignal/library" },
"fallback": { "dir": "/var/lib/weavesignal/fallback", "default_file": "/opt/weavesignal/assets/default-loop/default-loop.flac" },
"processing": { "enabled": ${PROCESSING:-true}, "target_lufs": ${PROCESSING_TARGET_LUFS:--16}, "max_gain_db": ${PROCESSING_MAX_GAIN_DB:-6} },
"logger": { "dir": "/var/lib/weavesignal/recordings/logger" }
}
desk:
content: |
[livekit]
url = "ws://127.0.0.1:${LIVEKIT_HTTP_PORT:-7880}"
api_key = "${LIVEKIT_API_KEY}"
room = "${ROOM:-studio}"
[programme]
srt_bind = "127.0.0.1"
srt_port = ${SRT_INGEST_PORT:-7313}
[control]
socket = "/run/weavesignal/desk.sock"
[carts]
dir = "/var/lib/weavesignal/carts"
# Video (ADR-0009): off unless VIDEO=true. NVENC needs the GPU handed to the desk's
# container (devices: nvidia.com/gpu=all); without it, SVT-AV1 on the CPU.
[video]
enabled = ${VIDEO:-false}
station = "${STATION_NAME:-Kettle FM}"
[video.hls]
dir = "/var/lib/weavesignal/hls"
# Playout's output back to the desk: the video's sound while nobody is live (ADR-0172).
[video.playout_feed]
port = ${DESK_FEED_PORT:-7317}
control:
content: |
[station]
id = "${STATION_ID}"
name = "${STATION_NAME:-Kettle FM}"
[ports]
control = ${CONTROL_PORT:-7310}
chat = ${CHAT_PORT:-7311}
listeners = ${LISTENERS_PORT:-7312}
playout_feed = ${PLAYOUT_FEED_PORT:-7315}
desk_feed = ${DESK_FEED_PORT:-7317}
srt_ingest = ${SRT_INGEST_PORT:-7313}
livekit_http = ${LIVEKIT_HTTP_PORT:-7880}
livekit_media = ${LIVEKIT_MEDIA_PORT:-7882}
proxy_http = ${PROXY_HTTP_PORT:-80}
proxy_https = ${PROXY_HTTPS_PORT:-443}
# The join pages put this port into the TURN address LiveKit reports (it always says 443).
turn_tls = ${TURN_TLS_PORT:-5349}
[room]
# As browsers reach LiveKit: through the proxy on the public route.
public_url = "${ROOM_PUBLIC_URL:-ws://127.0.0.1:${LIVEKIT_HTTP_PORT:-7880}}"
api_url = "http://127.0.0.1:${LIVEKIT_HTTP_PORT:-7880}"
api_key = "${LIVEKIT_API_KEY}"
name = "${ROOM:-studio}"
[desk]
socket = "/run/weavesignal/desk.sock"
[playout]
socket = "/run/weavesignal-playout/playout.sock"
listeners_socket = "/run/weavesignal-listeners/listeners.sock"
library_dir = "/var/lib/weavesignal/library"
[carts]
media_dir = "/var/lib/weavesignal/carts"
demo_pack = "/opt/weavesignal/carts-demo"
# The demo station's slate picture (ADR-0200), given once when the station has none;
# weavesignal.sh sets it only for a station left with the demo name.
[video]
demo_still = "${DEMO_STILL:-}"
# The public pages, from the image (M3): the player with call-in at /, invites at /join/.
# `listed` is whether search engines may list the public address (ADR-0133).
[web]
dir = "/opt/weavesignal/web"
listed = ${SEARCH_LISTING:-false}
# Show cards and podcast feeds (M8): every card prints the station's address and the feeds
# are reached under it, so they are off until the station is public (weavesignal.sh --public).
[publishing]
public_url = "${PUBLIC_ORIGIN:-}"
# How a caller asks for their call to be taken down: an email address or a link, said in
# the consent words. Call-in stays closed until it is set (ADR-0124).
[join]
consent_contact = "${CONSENT_CONTACT:-}"
# The player's Listen live: the listener process on loopback, or through the proxy.
stream_url = "${STREAM_URL:-http://127.0.0.1:${LISTENERS_PORT:-7312}/programme.opus}"
# True only behind the station's own proxy, which sets X-Forwarded-For (otherwise every
# caller shares the proxy's address and the per-address call-in limit caps the station).
forwarded_for = ${FORWARDED_FOR:-false}
# Staff sign-in by an emailed code (ADR-0127), beside the password: on when the station
# can send email, as for the chat's codes (MAIL_MODE, MAIL_FROM).
[mail]
mode = "${MAIL_MODE:-off}"
from = "${MAIL_FROM:-chat@station.example}"
chat:
content: |
[station]
id = "${STATION_ID}"
name = "${STATION_NAME:-Kettle FM}"
[listen]
address = "127.0.0.1"
port = ${CHAT_PORT:-7311}
forwarded_for = ${FORWARDED_FOR:-false}
[control]
url = "http://127.0.0.1:${CONTROL_PORT:-7310}"
[mail]
mode = "${MAIL_MODE:-off}"
from = "${MAIL_FROM:-chat@station.example}"