Merge branch 'stable' into stable-android

This commit is contained in:
Evgeny Poberezkin
2026-09-01 21:29:51 +01:00
35 changed files with 1197 additions and 501 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/linu
mkdir $vlc_dir || exit 0
vlc_tag='v3.0.21-1'
vlc_tag='v3.0.23-2'
vlc_url="https://github.com/simplex-chat/vlc/releases/download/${vlc_tag}/vlc-linux-${ARCH}.appimage"
cd /tmp
+1 -1
View File
@@ -10,7 +10,7 @@ else
vlc_arch=intel64
fi
vlc_tag='v3.0.21-1'
vlc_tag='v3.0.23-2'
vlc_url="https://github.com/simplex-chat/vlc/releases/download/${vlc_tag}/vlc-macos-${ARCH}.zip"
function readlink() {
+1 -1
View File
@@ -10,7 +10,7 @@ vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/wind
rm -rf $vlc_dir
mkdir -p $vlc_dir/vlc || exit 0
vlc_tag='v3.0.21-1'
vlc_tag='v3.0.23-2'
vlc_url="https://github.com/simplex-chat/vlc/releases/download/${vlc_tag}/vlc-win-x86_64.zip"
cd /tmp
+4
View File
@@ -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
+21
View File
@@ -0,0 +1,21 @@
# Copy to .env and edit.
RELAY_NAME="My Relay"
RELAY_WEB_DOMAIN=relay1.example.com
POSTGRES_USER=simplex
POSTGRES_DB=simplex_chat_relay
POSTGRES_PASSWORD=change-me
# Ref to build: tag, branch or commit.
CHAT_REF=88df79d1e26921c7d1835fc8484fade596356fb6
# SMP server for the relay address, used when the address is created.
#RELAY_ADDRESS_SERVER=smp://<fingerprint>@smp.example.com
# GHC runtime options, without the +RTS/-RTS markers.
#RELAY_RTS_OPTS="-N -F1.2 -A16m -I0.01 -Iw15"
# Database connection pool and internal queue size.
#RELAY_POOL_SIZE=4
#RELAY_QUEUE_SIZE=65536
+70
View File
@@ -0,0 +1,70 @@
# syntax=docker/dockerfile:1
ARG CHAT_REF=88df79d1e26921c7d1835fc8484fade596356fb6
ARG GHC=9.6.3
# 3.10.1.0 predates the Hackage root key rotation and fails `cabal update`.
ARG CABAL=3.10.2.0
# ---- build ----------------------------------------------------------------
FROM ubuntu:22.04 AS build
ARG GHC
ARG CABAL
ENV DEBIAN_FRONTEND=noninteractive \
PATH="/root/.cabal/bin:/root/.ghcup/bin:$PATH"
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/*
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}"
# Kept out of the layer above so that a failure here does not reinstall GHC.
# Retried because a failed fetch falls back to the mirrors in Hackage's
# mirrors.json, which are dead, and the last one aborts the build.
RUN cabal update || { sleep 5; cabal update; } || { sleep 20; cabal update; }
# 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 . \
&& git remote add origin https://github.com/simplex-chat/simplex-chat \
&& git fetch -q --depth 1 origin "${CHAT_REF}" \
&& git checkout -q FETCH_HEAD
# 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 \
&& 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
# 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 \
&& 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
# Bind-mounted host directories must be writable by this UID.
USER relay
# The relay shuts down cleanly on SIGINT, not SIGTERM.
STOPSIGNAL SIGINT
ENTRYPOINT ["python3", "/usr/local/bin/entrypoint.py"]
+87
View File
@@ -0,0 +1,87 @@
name: simplex-chat-relay
services:
relay:
build:
context: .
args:
CHAT_REF: ${CHAT_REF:-88df79d1e26921c7d1835fc8484fade596356fb6}
image: chat-relay:latest # set your image/registry name
container_name: chat-relay
depends_on:
db:
condition: service_healthy
environment:
RELAY_NAME: ${RELAY_NAME}
RELAY_WEB_DOMAIN: ${RELAY_WEB_DOMAIN}
RELAY_ADDRESS_SERVER: ${RELAY_ADDRESS_SERVER:-}
# Empty values fall back to the entrypoint defaults.
RELAY_RTS_OPTS: ${RELAY_RTS_OPTS:-}
RELAY_POOL_SIZE: ${RELAY_POOL_SIZE:-}
RELAY_QUEUE_SIZE: ${RELAY_QUEUE_SIZE:-}
# Password is passed as PGPASSWORD to keep it out of argv.
DB_CONN: postgresql://${POSTGRES_USER}@db:5432/${POSTGRES_DB}
PGPASSWORD: ${POSTGRES_PASSWORD}
volumes:
- /var/www/relay-web-channels:/var/www/relay-web-channels
- ./out:/out
restart: unless-stopped
metrics:
image: burningalchemist/sql_exporter:latest
container_name: chat-relay-metrics
depends_on:
db:
condition: service_healthy
environment:
# Percent-encode the password if it contains URL-reserved characters.
SQLEXPORTER_TARGET_DSN: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?sslmode=disable
volumes:
- ./sql_exporter.yml:/etc/sql_exporter/sql_exporter.yml:ro
command: ["-config.file=/etc/sql_exporter/sql_exporter.yml"]
ports:
- "127.0.0.1:9399:9399"
restart: unless-stopped
db:
image: postgres:18
container_name: chat-relay-db
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
shm_size: 256mb # parallel workers need more than the 64 MB default
volumes:
# 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"]
interval: 5s
timeout: 5s
retries: 10
# Tuning for ~5 GB RAM / 4 cores. Match to the actual server.
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:
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""Save the relay address on first start, then exec the relay.
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
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
DEFAULT_RTS_OPTS = "-N -F1.2 -A16m -I0.01 -Iw15"
DEFAULT_POOL_SIZE = "4" # the binary defaults to a single connection
DEFAULT_QUEUE_SIZE = "65536"
def require(name):
value = os.environ.get(name)
if not value:
sys.exit(f"{name} is required")
return value
def rts_args():
"""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)
if o not in ("+RTS", "-RTS")
]
return ["+RTS", *opts, "-RTS"] if opts else []
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):
"""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(
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)
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]
# Only applied when the address is created, which the one-shot below does.
address_server = os.environ.get("RELAY_ADDRESS_SERVER")
if address_server:
common += ["--relay-address-server", address_server]
# 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)
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",
"--pool-size", os.environ.get("RELAY_POOL_SIZE") or DEFAULT_POOL_SIZE,
"--queue-size", os.environ.get("RELAY_QUEUE_SIZE") or DEFAULT_QUEUE_SIZE,
] + rts_args()
os.execvp(relay[0], relay)
if __name__ == "__main__":
main()
+71
View File
@@ -0,0 +1,71 @@
global:
min_interval: 0s
max_connections: 3
max_idle_connections: 3
target:
name: chat_relay
# Replaced by SQLEXPORTER_TARGET_DSN. A non-empty value is required here
# because the DSN is validated before environment variables are applied.
data_source_name: "postgresql://replaced-by-env"
collectors: [chat_relay]
collectors:
- collector_name: chat_relay
metrics:
- metric_name: chat_relay_channels
type: gauge
help: "Channels by relay status."
key_labels: [status]
values: [channels]
query: |
SELECT relay_own_status AS status, count(*) AS channels
FROM simplex_v1_chat_schema.groups
WHERE relay_own_status IS NOT NULL
GROUP BY relay_own_status
- metric_name: chat_relay_published_channels
type: gauge
help: "Served channels that have a public address."
values: [channels]
query: |
SELECT count(*) AS channels
FROM simplex_v1_chat_schema.groups g
JOIN simplex_v1_chat_schema.group_profiles gp
ON gp.group_profile_id = g.group_profile_id
WHERE gp.public_group_id IS NOT NULL
AND g.relay_own_status IN ('active', 'accepted')
- metric_name: chat_relay_members
type: gauge
help: "Members across all channels."
values: [members]
query: |
SELECT count(*) AS members FROM simplex_v1_chat_schema.group_members
- metric_name: chat_relay_pending_deliveries
type: gauge
help: "Messages queued for delivery."
values: [deliveries]
query: |
SELECT count(*) AS deliveries
FROM simplex_v1_chat_schema.msg_deliveries
WHERE delivery_status = 'snd_pending'
- metric_name: chat_relay_oldest_pending_delivery_seconds
type: gauge
help: "Age of the oldest message queued for delivery."
values: [age]
query: |
SELECT COALESCE(EXTRACT(EPOCH FROM (now() - min(created_at))), 0) AS age
FROM simplex_v1_chat_schema.msg_deliveries
WHERE delivery_status = 'snd_pending'
- metric_name: chat_relay_messages_24h
type: gauge
help: "Chat items created in the last 24 hours."
values: [messages]
query: |
SELECT count(*) AS messages
FROM simplex_v1_chat_schema.chat_items
WHERE created_at > now() - interval '24 hours'
+2 -1
View File
@@ -36,7 +36,8 @@ mkdir -p "${init_dir}/${TAG}-${repo_name}/from-source" "${init_dir}/${TAG}-${rep
git -C "${tempdir}" clone "${repo}.git" &&\
cd "${tempdir}/${repo_name}" &&\
git checkout "${TAG}"
git checkout "${TAG}" &&\
git submodule update --init --recursive
oses="22.04@sha256:5c8b2c0a6c745bc177669abfaa716b4bc57d58e2ea3882fb5da67f4d59e3dda5 24.04@sha256:98ff7968124952e719a8a69bb3cccdd217f5fe758108ac4f21ad22e1df44d237"