mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-29 03:18:37 +00:00
scripts: add chat relay docker setup
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
# The Dockerfile clones the source itself and only needs entrypoint.py from the
|
||||
# build context. Exclude everything else, especially .env (secrets) and out/.
|
||||
*
|
||||
!entrypoint.py
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copy to .env and edit per server (relay1 / relay2 / relay3).
|
||||
|
||||
# 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 tag to build (relay mode requires v7.0.0-beta.4 or later).
|
||||
CHAT_REF=v7.0.0-beta.5
|
||||
@@ -0,0 +1,70 @@
|
||||
# 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=v7.0.0-beta.5
|
||||
ARG GHC=9.6.3
|
||||
ARG CABAL=3.10.1.0
|
||||
|
||||
# ---- build ----------------------------------------------------------------
|
||||
FROM ubuntu:22.04 AS build
|
||||
|
||||
ARG GHC
|
||||
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}" \
|
||||
BOOTSTRAP_HASKELL_CABAL_VERSION="${CABAL}" sh \
|
||||
&& ghcup set ghc "${GHC}" \
|
||||
&& ghcup set cabal "${CABAL}" \
|
||||
&& cabal update
|
||||
|
||||
# CHAT_REF is declared here, after the toolchain layers, so changing the tag
|
||||
# reuses everything above and rebuilds only the clone + compile below.
|
||||
ARG CHAT_REF
|
||||
RUN git clone --depth 1 --branch "${CHAT_REF}" \
|
||||
https://github.com/simplex-chat/simplex-chat /project
|
||||
WORKDIR /project
|
||||
|
||||
# Compile the executable with PostgreSQL persistence and strip it. The cabal
|
||||
# store (compiled dependencies) and dist-newstyle are cache mounts, so a
|
||||
# different tag recompiles only changed code, not every dependency.
|
||||
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 \
|
||||
&& cabal build -fclient_postgres exe:simplex-chat \
|
||||
&& bin=$(find dist-newstyle -name simplex-chat -type f -executable | head -n1) \
|
||||
&& install -m 0755 "$bin" /simplex-chat-relay \
|
||||
&& strip /simplex-chat-relay
|
||||
|
||||
# ---- runtime --------------------------------------------------------------
|
||||
FROM debian:stable-slim AS runtime
|
||||
|
||||
# python3 runs the entrypoint; the rest are the binary's shared libraries.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates python3 \
|
||||
libpq5 libgmp10 libssl3 zlib1g libnuma1 libffi8 \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& useradd -m -u 1000 relay
|
||||
|
||||
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.
|
||||
USER relay
|
||||
|
||||
# The relay (GHC runtime) shuts down cleanly on SIGINT, not SIGTERM.
|
||||
STOPSIGNAL SIGINT
|
||||
ENTRYPOINT ["python3", "/usr/local/bin/entrypoint.py"]
|
||||
@@ -0,0 +1,74 @@
|
||||
name: simplex-chat-relay
|
||||
|
||||
services:
|
||||
relay:
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
CHAT_REF: ${CHAT_REF:-v7.0.0-beta.5}
|
||||
image: chat-relay:latest # set your image/registry name
|
||||
container_name: chat-relay # optional: rename
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
RELAY_NAME: ${RELAY_NAME}
|
||||
RELAY_WEB_DOMAIN: ${RELAY_WEB_DOMAIN}
|
||||
# Password comes from PGPASSWORD (libpq reads it), so it stays out of the
|
||||
# process argv and needs no URL-encoding.
|
||||
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
|
||||
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
|
||||
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.
|
||||
- pgdata:/var/lib/postgresql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
|
||||
interval: 5s
|
||||
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
|
||||
- --shared_buffers=1280MB
|
||||
- --effective_cache_size=3840MB
|
||||
- --maintenance_work_mem=320MB
|
||||
- --checkpoint_completion_target=0.9
|
||||
- --wal_buffers=16MB
|
||||
- --default_statistics_target=100
|
||||
- --random_page_cost=1.1
|
||||
- --effective_io_concurrency=200
|
||||
- --work_mem=6301kB
|
||||
- --huge_pages=off
|
||||
- --jit=off
|
||||
- --wal_compression=lz4
|
||||
- --min_wal_size=1GB
|
||||
- --max_wal_size=4GB
|
||||
- --max_worker_processes=4
|
||||
- --max_parallel_workers_per_gather=2
|
||||
- --max_parallel_workers=4
|
||||
- --max_parallel_maintenance_workers=2
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
Executable
+100
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Record the relay address, then run the relay with no wrapper in the way.
|
||||
|
||||
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.
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
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
|
||||
|
||||
|
||||
def require(name):
|
||||
value = os.environ.get(name)
|
||||
if not value:
|
||||
sys.exit(f"{name} is required")
|
||||
return value
|
||||
|
||||
|
||||
def find_address(text):
|
||||
for token in text.split():
|
||||
if token.startswith("https://") or token.startswith("simplex:"):
|
||||
return token
|
||||
return None
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
cmd = oneshot + ["--create-schema", "-t", "0", "-e", "/sa"]
|
||||
try:
|
||||
out = subprocess.run(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
errors="replace",
|
||||
timeout=CAPTURE_TIMEOUT,
|
||||
).stdout
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
out = exc.stdout or ""
|
||||
sys.stdout.write(out) # keep it in the container logs
|
||||
sys.stdout.flush()
|
||||
|
||||
address = find_address(out)
|
||||
if address:
|
||||
with open(ADDR_FILE, "w") as f:
|
||||
f.write(address + "\n")
|
||||
else:
|
||||
sys.stderr.write("entrypoint: relay address not captured; will retry next start\n")
|
||||
|
||||
|
||||
def main():
|
||||
name = require("RELAY_NAME")
|
||||
domain = require("RELAY_WEB_DOMAIN")
|
||||
conn = require("DB_CONN")
|
||||
image_file = os.environ.get("RELAY_IMAGE_FILE")
|
||||
|
||||
os.makedirs(f"{WEB_ROOT}/channel", exist_ok=True)
|
||||
os.makedirs("/out", exist_ok=True)
|
||||
|
||||
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.
|
||||
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",
|
||||
"--relay-web-cors-file", f"{WEB_ROOT}/cors.conf",
|
||||
"--relay-web-interval", "30",
|
||||
"-d", conn,
|
||||
"--create-schema",
|
||||
]
|
||||
os.execvp(relay[0], relay)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user