diff --git a/scripts/relay/.env.example b/scripts/relay/.env.example index b8d7a84560..7d9100378d 100644 --- a/scripts/relay/.env.example +++ b/scripts/relay/.env.example @@ -1,16 +1,14 @@ -# Copy to .env and edit per server (relay1 / relay2 / relay3). +# Copy to .env and edit. -# Relay identity. RELAY_NAME="My Relay" RELAY_WEB_DOMAIN=relay1.example.com -# PostgreSQL credentials (used by both the db service and the relay's DB_CONN). POSTGRES_USER=simplex POSTGRES_DB=simplex_chat_relay POSTGRES_PASSWORD=change-me -# Source ref to build: tag, branch or commit. +# Ref to build: tag, branch or commit. CHAT_REF=88df79d1e26921c7d1835fc8484fade596356fb6 -# GHC runtime options for the relay. Uncomment to override the default. +# GHC runtime options, without the +RTS/-RTS markers. #RELAY_RTS_OPTS="-N -F1.2 -A16m -I0.01 -Iw15" diff --git a/scripts/relay/Dockerfile b/scripts/relay/Dockerfile index f439124e5f..437473c6ba 100644 --- a/scripts/relay/Dockerfile +++ b/scripts/relay/Dockerfile @@ -1,7 +1,4 @@ # syntax=docker/dockerfile:1 -# Build simplex-chat in relay mode with PostgreSQL support, then ship it on a -# slim runtime with only the libraries the binary links against. A bare -# `scratch` image cannot run this dynamically-linked binary. ARG CHAT_REF=88df79d1e26921c7d1835fc8484fade596356fb6 ARG GHC=9.6.3 @@ -15,14 +12,12 @@ ARG CABAL ENV DEBIAN_FRONTEND=noninteractive \ PATH="/root/.cabal/bin:/root/.ghcup/bin:$PATH" -# Build toolchain and simplex-chat dependencies (libpq-dev for client_postgres). RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates curl git build-essential \ libpq-dev libgmp3-dev zlib1g-dev libnuma-dev libssl-dev \ llvm-12 llvm-12-dev \ && rm -rf /var/lib/apt/lists/* -# GHC + cabal via ghcup, then fetch the package index into this layer. RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org \ | BOOTSTRAP_HASKELL_NONINTERACTIVE=1 \ BOOTSTRAP_HASKELL_GHC_VERSION="${GHC}" \ @@ -31,9 +26,8 @@ RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org \ && ghcup set cabal "${CABAL}" \ && cabal update -# CHAT_REF is declared here, after the toolchain layers, so changing the ref -# reuses everything above and rebuilds only the fetch + compile below. The -# fetch form accepts a commit hash as well as a tag or branch. +# Declared after the toolchain layers so that changing the ref does not rebuild +# them. Unlike `clone --branch`, this form also accepts a commit hash. ARG CHAT_REF WORKDIR /project RUN git init -q . \ @@ -41,9 +35,7 @@ RUN git init -q . \ && git fetch -q --depth 1 origin "${CHAT_REF}" \ && git checkout -q FETCH_HEAD -# Compile the executable with PostgreSQL persistence and strip it. The cabal -# store (compiled dependencies) and dist-newstyle are cache mounts, so a -# different ref recompiles only changed code, not every dependency. +# The cache mounts keep compiled dependencies across ref changes. RUN --mount=type=cache,target=/root/.cabal/store,sharing=locked \ --mount=type=cache,target=/project/dist-newstyle,sharing=locked \ cp scripts/cabal.project.local.linux cabal.project.local \ @@ -55,7 +47,7 @@ RUN --mount=type=cache,target=/root/.cabal/store,sharing=locked \ # ---- runtime -------------------------------------------------------------- FROM debian:stable-slim AS runtime -# python3 runs the entrypoint; the rest are the binary's shared libraries. +# The binary is dynamically linked, so it needs these libraries at runtime. RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates python3 \ libpq5 libgmp10 libssl3 zlib1g libnuma1 libffi8 \ @@ -65,9 +57,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ COPY --from=build /simplex-chat-relay /usr/local/bin/simplex-chat-relay COPY entrypoint.py /usr/local/bin/entrypoint.py -# Run as non-root; host bind-mounts must be writable by this UID. +# Bind-mounted host directories must be writable by this UID. USER relay -# The relay (GHC runtime) shuts down cleanly on SIGINT, not SIGTERM. +# The relay shuts down cleanly on SIGINT, not SIGTERM. STOPSIGNAL SIGINT ENTRYPOINT ["python3", "/usr/local/bin/entrypoint.py"] diff --git a/scripts/relay/docker-compose.yaml b/scripts/relay/docker-compose.yaml index b736a2ec2d..d6ffe4ffb6 100644 --- a/scripts/relay/docker-compose.yaml +++ b/scripts/relay/docker-compose.yaml @@ -7,40 +7,32 @@ services: args: CHAT_REF: ${CHAT_REF:-88df79d1e26921c7d1835fc8484fade596356fb6} image: chat-relay:latest # set your image/registry name - container_name: chat-relay # optional: rename + container_name: chat-relay depends_on: db: condition: service_healthy environment: RELAY_NAME: ${RELAY_NAME} RELAY_WEB_DOMAIN: ${RELAY_WEB_DOMAIN} - # Empty falls back to the entrypoint's default RTS options. - RELAY_RTS_OPTS: ${RELAY_RTS_OPTS:-} - # Password comes from PGPASSWORD (libpq reads it), so it stays out of the - # process argv and needs no URL-encoding. + RELAY_RTS_OPTS: ${RELAY_RTS_OPTS:-} # empty: entrypoint default + # Password is passed as PGPASSWORD to keep it out of argv. DB_CONN: postgresql://${POSTGRES_USER}@db:5432/${POSTGRES_DB} PGPASSWORD: ${POSTGRES_PASSWORD} volumes: - # Channel previews + CORS config, served by the host's system Caddy. - /var/www/relay-web-channels:/var/www/relay-web-channels - # Relay address is written here: `cat out/relay-address.txt`. - ./out:/out restart: unless-stopped db: image: postgres:18 - container_name: chat-relay-db # optional: rename + container_name: chat-relay-db environment: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_DB: ${POSTGRES_DB} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - # Parallel workers use POSIX shared memory; the default 64 MB /dev/shm can - # be too small and cause "could not resize shared memory segment" errors. - shm_size: 256mb + shm_size: 256mb # parallel workers need more than the 64 MB default volumes: - # PG18+ uses a version-specific PGDATA (/var/lib/postgresql/18/docker) and - # a VOLUME at /var/lib/postgresql. Mount there so major-version upgrades - # can use pg_upgrade --link. + # PG18+ keeps PGDATA in a version-specific subdirectory of this volume. - pgdata:/var/lib/postgresql healthcheck: test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] @@ -48,7 +40,6 @@ services: timeout: 5s retries: 10 # Tuning for ~5 GB RAM / 4 cores. Match to the actual server. - # `--name=value` is postgres's equivalent of `-c name=value`. command: - postgres - --max_connections=100 diff --git a/scripts/relay/entrypoint.py b/scripts/relay/entrypoint.py index 837aa07a4f..a63d23d9ee 100755 --- a/scripts/relay/entrypoint.py +++ b/scripts/relay/entrypoint.py @@ -1,15 +1,8 @@ #!/usr/bin/env python3 -"""Record the relay address, then run the relay with no wrapper in the way. +"""Save the relay address on first start, then exec the relay. -On the first start (or whenever /out/relay-address.txt is missing) a short -one-shot invocation creates the profile and address if needed, prints the -address, and saves it. Then the real relay is exec'd, so it runs as PID 1 with -no Python left in the process tree. Steady-state starts skip straight to the -exec. - -The address is not stored in the database as a ready-to-use string (it is a -binary conn-req blob plus short-link data, re-encoded on display), so the -relay's own output is the canonical source. +The address is printed only when it is created, and is not stored in the +database in its displayed form, so it is captured from the relay's output. """ import os import shlex @@ -19,10 +12,7 @@ import sys BIN = "simplex-chat-relay" WEB_ROOT = "/var/www/relay-web-channels" ADDR_FILE = "/out/relay-address.txt" -CAPTURE_TIMEOUT = 180 # seconds; first address creation involves an SMP round-trip - -# GHC runtime options for the relay, overridable with RELAY_RTS_OPTS. The binary -# is built with -rtsopts, so it accepts them on the command line. +CAPTURE_TIMEOUT = 180 # seconds DEFAULT_RTS_OPTS = "-N -F1.2 -A16m -I0.01 -Iw15" @@ -34,11 +24,7 @@ def require(name): def rts_args(): - """Configured RTS options, wrapped in the +RTS/-RTS markers. - - RELAY_RTS_OPTS holds bare options ("-N -A16m"); the markers are added here - and ignored if they are given anyway. - """ + """RELAY_RTS_OPTS holds bare options; the +RTS/-RTS markers are added here.""" opts = [ o for o in shlex.split(os.environ.get("RELAY_RTS_OPTS") or DEFAULT_RTS_OPTS) @@ -55,13 +41,7 @@ def find_address(text): def capture_address(oneshot): - """One-shot: create the profile/address if needed, print it, and save it. - - Runs before the real relay starts, so there is never a second agent - subscribing to the same queues. If it can't capture (e.g. SMP briefly - unreachable), the relay still creates and serves the address, and the next - start retries because the file is still missing. - """ + """Create the address if needed and save it. Runs before the relay starts.""" cmd = oneshot + ["--create-schema", "-t", "0", "-e", "/sa"] try: out = subprocess.run( @@ -74,7 +54,7 @@ def capture_address(oneshot): ).stdout except subprocess.TimeoutExpired as exc: out = exc.stdout or "" - sys.stdout.write(out) # keep it in the container logs + sys.stdout.write(out) sys.stdout.flush() address = find_address(out) @@ -96,14 +76,11 @@ def main(): common = [BIN, "--relay", "--headless", "--user-display-name", name] - # First start (or the file was removed): bootstrap and capture the address, - # then the real relay reuses it. The avatar only applies at profile - # creation, so it goes on the one-shot, not the long-running relay. + # The image is applied only when the profile is created. if not os.path.exists(ADDR_FILE): oneshot = common + (["--user-image-file", image_file] if image_file else []) + ["-d", conn] capture_address(oneshot) - # Replace this process with the relay: PID 1, native signals, no wrapper. relay = common + [ "--relay-web-domain", domain, "--relay-web-dir", f"{WEB_ROOT}/channel",