Status: operation. Scope: complete single-host Docker Compose topology for
file/core/egress/sfu/edge. Chinese version:docker-deployment.md.
This deployment is not a compatibility mode for the old monolith. By default it starts PostgreSQL, Redis, a one-shot schema migrator, and five long-running application roles:
patched client
|
v
Edge :2398 ---> CoreExec/Core :2440 ---> PostgreSQL
| | ^
+----> FileData/File :2520 ------------+
|
+----> Egress ACK/Egress :2510 --------+
Core <----> SFU control :2450
|
+---- UDP :12399 ---> clients
Edge/Core/Egress/SFU <---- Redis control/location fabric
The Edge readiness probe checks the local MTProto listener, Redis, and the
authenticated gRPC health of CoreExec, FileData, and Egress ACK. The probes for
Core, File, Egress, and SFU also authenticate their direct PostgreSQL, Redis, or
service dependencies. If a required dependency is unavailable, the affected
container becomes unhealthy, and business RPCs do not fall back to in-process
execution. Docker Compose does not restart a container solely because it is
unhealthy; a production deployment still needs alerting and an operator or
external watchdog to perform recovery. FileData is mandatory. Do not remove the
File service or restore a local file fallback in Core or Edge.
- Docker Engine 24+ or Docker Desktop, with Compose v2.24+.
- At least 4 GiB of available memory. Use 8 GiB for local image builds and keep at least 2 GiB of free disk for the Core and File transcoding volumes.
- An IPv4 or IPv6 address reachable by the patched client. The embedded TURN listener is currently IPv4, so TURN deployments need a client-reachable IPv4.
- Network access to
ghcr.ioand Docker Hub for the default startup. Go module and Alpine package access is needed only for a local-Build.
The default localfs blob backend is suitable for a single-host deployment.
Multi-host operation, high availability, TLS/mTLS, external PostgreSQL/Redis,
S3, and load balancing are the next deployment tier. They cannot be achieved by
copying this Compose file or running --scale: fixed host ports, localfs, SFU
media addresses, and stable instance IDs all require orchestration-level design.
On Windows PowerShell 5.1+ or PowerShell 7+, run the generator. It uses the
system cryptographic random source, restricts the file ACL, and refuses to
overwrite an existing .env:
.\scripts\new-docker-env.ps1 `
-AdvertiseIP 192.0.2.10 `
-PublicBaseURL https://chat.example.com `
-PublicWebBaseURL https://web.chat.example.comFor local development, only -AdvertiseIP 127.0.0.1 is required. The generator
also restricts published ports to loopback and creates a local HTTP public URL.
A non-loopback address requires explicit self-hosted public URLs; the generator
does not silently retain project-owned domains.
On Linux or macOS, copy the template, then use openssl rand -hex 32 to create
independent database, Redis, and internal service secrets:
cp deploy/docker/.env.example deploy/docker/.env
chmod 600 deploy/docker/.env
${EDITOR:-vi} deploy/docker/.envCheck all of the following:
- No
.envassignment may retain aCHANGEMEplaceholder. The container entrypoint fails fast when one remains. TELESRV_ADVERTISE_IPmust be an IP address reachable by clients, not a Docker service name or DNS name.- IPv4 deployments enable TURN by default.
TELESRV_TURN_ADVERTISE_IPmust be a client-reachable IPv4 andTELESRV_TURN_SECRETmust be an independent random value. Forward the TURN listener and relay range without translating12500-12999/udpto a different external range. The generator explicitly disables embedded TURN for IPv6-only deployments. - RTMP is enabled by default.
TELESRV_LIVESTREAM_RTMP_URLmust be an OBS/publisher-reachablertmp://<host>:2400/liveURL. - Development login codes require
TELESRV_ALLOW_INSECURE_DEVELOPMENT_AUTH=true. The generator enables it automatically only for loopback. For temporary LAN testing, pass-AllowInsecureDevelopmentAuthexplicitly. An Internet-facing deployment must setTELESRV_PHONE_CODE_DELIVERY_PROVIDER=webhook,TELESRV_OTP_WEBHOOK_URL, and an independentTELESRV_OTP_WEBHOOK_SECRET; seeotp-delivery.md. - The password inside
TELESRV_POSTGRES_DSNmust matchPOSTGRES_PASSWORD. URL-encode special characters when editing it manually. - The five internal tokens and TURN secret must be independent and must not reuse a database, Redis, or administrator password.
The generator has no force-overwrite mode. Regenerating .env would make an
existing PostgreSQL password, Redis password, and container tokens inconsistent.
Rotate credentials one at a time with a coordinated restart; do not treat
initialization as a repeatable reset command.
Role YAML expands only ${NAME} placeholders that actually appear in the file.
Ordinary process environment variables do not override absent YAML fields.
Docker configuration is therefore defined by deploy/docker/config/*.yaml and
the role-specific Compose environment allowlists, not by an unlimited .env.
Each role image contains only its own YAML, and the default Compose topology does
not bind-mount configuration from the host. This keeps image and configuration
revisions aligned. Rebuild all affected images after changing tracked YAML. If a
deployment-specific override is required, mount one role file through a separate
Compose override and include that override in encrypted backup and same-version
rollback procedures.
For the first Windows experience, run the following from the repository root.
When .env is absent, the script initializes it with loopback defaults,
validates the configuration, pulls the v2 images from GHCR, starts the stack,
and waits for readiness:
.\scripts\start-docker.ps1When startup completes, the script prints the default development login code
12345, TURN/STUN endpoint, relay range, and RTMP ingest URL. Docker Desktop
may need several minutes to create the complete TURN UDP range on first startup;
wait until the script reports that the stack is ready. A temporary LAN deployment
can also be initialized with one command. Replace the example address with the
LAN address of the Docker host:
.\scripts\start-docker.ps1 -AdvertiseIP 192.168.1.20 -PublicBaseURL http://192.168.1.20:2401 -PublicWebBaseURL http://192.168.1.20:2401 -AllowInsecureDevelopmentAuthFor a real Internet-facing deployment, first run new-docker-env.ps1 by itself,
configure the phone-code webhook described in Section 2, and then run
start-docker.ps1 without address parameters. If .env already exists, the
startup script always reuses its credentials. New address or development-auth
parameters only produce a warning and never overwrite deployment identity. Edit
.env explicitly before restarting when an address must change.
The default image prefix is ghcr.io/iamxvbaba/gramsrv. It contains six
packages: migrate, file, core, egress, sfu, and edge. The convenient
channel tag is v2; every publish also produces an immutable sha-<commit>
tag. Automatic publication on v2 pushes is currently paused; maintainers run
the Publish v2 container images workflow manually from GitHub Actions. A newly
created GHCR package is private by default, so maintainers must set each of the
six packages to Public in GitHub Packages before anonymous one-click pulls
work. While they remain private, run docker login ghcr.io first.
To validate modified source with locally built images, opt in explicitly. The
Compose Edge build target is edge-test, matching the public v2 test channel:
.\scripts\start-docker.ps1 -BuildTo run the phases separately, or to start on Linux/macOS:
docker compose --project-directory deploy/docker --env-file deploy/docker/.env -f deploy/docker/compose.yaml config --quiet
docker compose --project-directory deploy/docker --env-file deploy/docker/.env -f deploy/docker/compose.yaml pull
docker compose --project-directory deploy/docker --env-file deploy/docker/.env -f deploy/docker/compose.yaml up -d --no-build --waitAlternatively, enter deploy/docker first:
Set-Location deploy\docker
docker compose config --quiet
docker compose pull
docker compose up -d --no-build --waitFor an empty database, PostgreSQL and Redis start in parallel. The one-shot
migrate job runs after PostgreSQL is healthy. After migration succeeds, File
starts, and Egress starts once Redis is also healthy. Core starts after File is
healthy; SFU and Edge can satisfy their own start conditions only after Core is
healthy. Edge waits for Redis, Core, File, and Egress, but intentionally does not
wait for SFU so that media availability does not block the messaging entrypoint.
This sequence prevents multiple long-running processes from racing migrations
that include CREATE INDEX CONCURRENTLY. File, Core, and Egress still confirm
the same migrations idempotently as a second gate. up --wait requires every
long-running service to be healthy and the migrator to finish successfully.
Each application role uses its own Dockerfile target. An image contains only its
role binary, the health probe, its YAML, selected seeds, and required runtime
libraries. Base images are pinned by digest. Application containers run as UID 10001 with a
read-only root filesystem, all capabilities dropped, a 512 PID limit, and
no-new-privileges.
Inspect status and ready logs:
docker compose --project-directory deploy/docker -f deploy/docker/compose.yaml ps --all
docker compose --project-directory deploy/docker -f deploy/docker/compose.yaml logs --tail 100 file core egress sfu edgeExpected state: migrate is Exited (0), all five long-running applications
are healthy, and the logs include:
telesrv-migrate: schema ready ...telesrv file readytelesrv core role readyturn listeninglive stream rtmp ingest listeningtelesrv egress ready- SFU standalone ready/heartbeat messages
telesrv edge ready
| Host port | Default binding | Purpose |
|---|---|---|
2398/tcp |
127.0.0.1 for zero-argument setup; 0.0.0.0/:: for non-loopback initialization |
MTProto Edge client entrypoint |
12399/udp |
127.0.0.1 for zero-argument setup; 0.0.0.0/:: for non-loopback initialization |
SFU media |
12400/udp |
127.0.0.1 for zero-argument setup; 0.0.0.0 for non-loopback IPv4 initialization |
TURN/STUN listener |
12500-12999/udp |
Same binding as 12400/udp; 1:1 host/container ports |
TURN relay allocations |
2400/tcp |
127.0.0.1 for zero-argument setup; 0.0.0.0/:: for non-loopback initialization |
RTMP ingest for OBS or another publisher |
2401/tcp |
127.0.0.1 by default; the advertised IP for a direct HTTP URL |
Public-link HTTP or host nginx/Caddy reverse proxy |
PostgreSQL, Redis, CoreExec, FileData, Egress ACK, SFU control, and debug/pprof
are not published to the host. data and control are internal Docker
networks. Internal gRPC currently uses bearer tokens over plaintext, so the
Docker daemon, host root, and containers on the same control network are part of
the trust boundary. A cross-host deployment needs a TLS/mTLS overlay and cannot
reuse these probes unchanged.
IPv4 Docker deployments enable embedded TURN and RTMP by default. The router/NAT
and Windows/Linux firewall must both allow 12400/udp, the complete
12500-12999/udp range, and 2400/tcp. The TURN relay range requires 1:1
forwarding and TELESRV_TURN_ADVERTISE_IP must be the address clients can reach;
changing only .env or only the firewall is not a complete forwarding setup.
The v2 compose.yaml already configures the listeners and published ports from
.env; no manual Compose edit is needed. Run the startup script again after
changing these variables so Compose recreates Core when required.
On a host with multiple network interfaces, set TELESRV_PUBLIC_BIND_IP and
the IPv4 TURN TELESRV_TURN_BIND_IP to specific addresses. When the generator
sees a PublicBaseURL in the form
http://<AdvertiseIP>:..., it binds port 2401 directly to that address for a
simple LAN experience. For an Internet-facing reverse proxy, keep 2401 bound to
host loopback and terminate TLS in nginx or Caddy. Never publish pprof or
internal gRPC directly.
Bot API, Admin, and the TON worker are not enabled by the default Compose topology; add explicit configuration and validation when needed.
| Volume | Contents | Consequence if lost |
|---|---|---|
postgres_data |
All durable business, authorization, update facts, and schema | Service data is lost |
redis_data |
Location/control/cache state and AOF | Online state is rebuilt; active sessions are affected |
file_data |
localfs blobs, upload parts, and map cache | Media cannot be recovered |
file_tmp |
File GIF/video transcoding temporary files | Safe to recreate; do not back up |
core_state |
Optional seeds, map cache, and OIDC key files | Enabled features fail or identity changes |
core_tmp |
Temporary files for ffmpeg/ffprobe and RTMP segments | Safe to recreate; do not back up |
edge_state |
MTProto RSA private and public keys | Patched clients can no longer complete the handshake |
A fresh deployment does not require the user to create or populate a host
data/ directory. Compose automatically creates the named volumes above and
writes the database, media, and RSA identity into them. With the default
COMPOSE_PROJECT_NAME=telesrv, the actual volume names look like
telesrv_postgres_data, telesrv_file_data, and telesrv_edge_state. Changing
the project name also changes the prefix.
List the current deployment volumes and inspect their Docker-managed host locations:
docker volume ls --filter label=com.docker.compose.project=telesrv
docker volume inspect telesrv_postgres_data
docker volume inspect telesrv_file_data
docker volume inspect telesrv_edge_stateDocker Engine on Linux usually stores named volumes under
/var/lib/docker/volumes/. Docker Desktop on Windows and macOS stores them in
the Docker Desktop virtual disk. Do not edit those internal directories
directly. Use PostgreSQL tools, the File container, docker compose cp, or the
backup and restore procedure in Section 6.
The repository-root data/ directory serves host-native development and image
build inputs:
internal/seed/catalogandinternal/seed/appearanceare compiled into Core throughgo:embed;data/langpack/is copied to/usr/share/telesrv/langpack/. They already ship in the GHCR Core image, so users do not prepare or mountdata/. The image manifest is/usr/share/telesrv/seed-manifest.json.- The hundreds of megabytes of sticker/reaction exports, official gifts, and Premium promo media are not in the default image. They remain optional, feature-specific imports so the one-click download stays reasonable.
- The current test pair is fixed at
deploy/docker/assets/test-server-rsa.pem.b64anddeploy/docker/assets/test-server-rsa.pub. It is copied only into theedge-testtarget and the GHCRedge:v2image. - Database, media blobs, and Redis state are not mapped into one host
data/directory. They are separated into role-owned named volumes. This retains data across container recreation and avoids Windows/Linux bind-mount permission differences during one-click startup.
Do not copy empty directories or sample files into the volumes for a fresh
deployment. An existing installation also cannot be migrated by overwriting the
whole data/ directory. Restore the database through dump/restore, media into
file_data, and the RSA private key into edge_state, as described in Section 6.
The default .env uses TELESRV_RSA_IDENTITY_MODE=test. On an empty
edge_state volume, the Edge entrypoint decodes the image's published test
private-key fixture once and exports the matching PKCS#1 public key. This fixed
pair is the current test-stage identity, and its private key is deliberately
public test data.
| File | Repository/image path | Runtime volume path | Purpose |
|---|---|---|---|
| RSA private key | deploy/docker/assets/test-server-rsa.pem.b64 / /usr/share/telesrv/keys/test-server-rsa.pem.b64 |
/var/lib/telesrv-edge/server_rsa.pem |
Base64 is only for publishing; runtime restores the original PKCS#1 key; do not put it in a client |
| RSA public key | deploy/docker/assets/test-server-rsa.pub / /usr/share/telesrv/keys/test-server-rsa.pub |
/var/lib/telesrv-edge/server_rsa.pub |
Embed in patched TDesktop or Android clients |
Clients using the default edge:v2 can therefore read
deploy/docker/assets/test-server-rsa.pub directly without waiting for the
server to start. To confirm the identity active in the volume, export it to
data/docker-server_rsa.pub, which is excluded from Git and the Docker build
context, and compare the files:
if (Test-Path data\docker-server_rsa.pub) { throw "data/docker-server_rsa.pub already exists; compare it before replacing" }
docker compose --project-directory deploy/docker --env-file deploy/docker/.env -f deploy/docker/compose.yaml cp edge:/var/lib/telesrv-edge/server_rsa.pub data/docker-server_rsa.pubLinux and macOS can use the same docker compose ... cp command. The entrypoint
never overwrites an existing edge_state/server_rsa.pem; recreating the
container therefore preserves its identity. If an old volume already contains
a different key, that identity stays active and Edge logs the difference. In
the default test mode, deleting the volume installs the same published test pair
again, so the test-client fingerprint stays stable.
For a production deployment, set TELESRV_RSA_IDENTITY_MODE=generated and
TELESRV_EDGE_BUILD_TARGET=edge in a new deployment, then run
.\scripts\start-docker.ps1 -Build to create a private-key-free Edge image; or
provision a custom PKCS#1 key into edge_state before startup. Back up that production key
and never commit it. Multiple Edge instances use the same production identity.
The published test private key in this section must not be treated as a secret.
file_tmp and core_tmp use host disk through named volumes instead of the
64 MiB /tmp tmpfs. Normal requests remove temporary files. A process crash can
leave telesrv-* files behind; monitor volume capacity and remove orphaned files
only while the corresponding service is stopped.
Stop business writers before taking a consistent backup. The following PowerShell workflow writes binary archives directly instead of passing them through Windows PowerShell 5.1 text redirection:
$backupDir = (New-Item -ItemType Directory -Force deploy\docker\backups\manual).FullName
docker compose --project-directory deploy/docker -f deploy/docker/compose.yaml stop edge sfu egress core file
docker compose --project-directory deploy/docker -f deploy/docker/compose.yaml exec -T postgres sh -ec 'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc -f /tmp/telesrv.dump'
docker compose --project-directory deploy/docker -f deploy/docker/compose.yaml cp postgres:/tmp/telesrv.dump "$backupDir/telesrv.dump"
docker compose --project-directory deploy/docker -f deploy/docker/compose.yaml exec -T postgres rm -f /tmp/telesrv.dump
docker compose --project-directory deploy/docker -f deploy/docker/compose.yaml cp edge:/var/lib/telesrv-edge/server_rsa.pem "$backupDir/server_rsa.pem"
docker compose --project-directory deploy/docker -f deploy/docker/compose.yaml run --rm --no-deps --user 0:0 --volume "${backupDir}:/backup" file tar -C /var/lib/telesrv-file -czf /backup/file_data.tar.gz .
Copy-Item -LiteralPath deploy\docker\.env -Destination "$backupDir/.env"Also back up Docker YAML overrides, explicitly enabled OIDC/seeds in
core_state, and the deployed image tags or digests. The backup directory is
excluded from Git and the build context, but it still needs owner-only ACLs,
off-host encryption, and checksums. With the S3 backend, do not treat
file_data as the blob source of truth. Use object-store versioning or
replication aligned with the database snapshot. Run up -d --wait after backup.
Always rehearse recovery in an isolated project with empty volumes. Restore the
backed-up .env, then run:
$backupDir = (Resolve-Path deploy\docker\backups\manual).Path
docker compose --project-directory deploy/docker -f deploy/docker/compose.yaml up -d --wait postgres redis
docker compose --project-directory deploy/docker -f deploy/docker/compose.yaml cp "$backupDir/telesrv.dump" postgres:/tmp/telesrv.dump
docker compose --project-directory deploy/docker -f deploy/docker/compose.yaml exec -T postgres sh -ec 'pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB" --clean --if-exists --no-owner /tmp/telesrv.dump'
docker compose --project-directory deploy/docker -f deploy/docker/compose.yaml run --rm --no-deps --user 0:0 --volume "${backupDir}:/backup:ro" file tar -C /var/lib/telesrv-file -xzf /backup/file_data.tar.gz
docker compose --project-directory deploy/docker -f deploy/docker/compose.yaml run --rm --no-deps --user 0:0 --volume "${backupDir}:/backup:ro" edge sh -ec 'install -m 0600 -o 10001 -g 10001 /backup/server_rsa.pem /var/lib/telesrv-edge/server_rsa.pem'
docker compose --project-directory deploy/docker -f deploy/docker/compose.yaml up -d --waitRestore separate backups for any explicitly enabled login keys or seeds in
core_state. Redis online state can be rebuilt from an empty volume, but clients
will reconnect. Finally verify the RSA fingerprint, schema, media range reads,
and real-client login. A backup that restores the database but omits blobs or
the RSA identity is not usable.
All five roles must come from the same source revision. Back up first, stop old writers, then build or pull every role:
docker compose --project-directory deploy/docker -f deploy/docker/compose.yaml stop edge sfu egress core file
docker compose --project-directory deploy/docker -f deploy/docker/compose.yaml pull
docker compose --project-directory deploy/docker -f deploy/docker/compose.yaml run --rm --no-deps migrate
docker compose --project-directory deploy/docker -f deploy/docker/compose.yaml up -d --wait file
docker compose --project-directory deploy/docker -f deploy/docker/compose.yaml up -d --wait core egress
docker compose --project-directory deploy/docker -f deploy/docker/compose.yaml up -d --wait sfu edgeThe standalone migrator applies the schema for the current revision first. File, Core, and Egress then confirm the same migrations during startup. This does not mean that every old binary is backward-compatible with the new schema. If a migration is incompatible, rollback must restore the pre-upgrade database and matching file/RSA state; changing only the image tag is insufficient.
Use docker compose stop or docker compose down for ordinary shutdown.
docker compose down -v deletes the database, media, and RSA identity and is
allowed only when intentionally resetting a test environment.
The default is TELESRV_BLOB_BACKEND=localfs, with File as the sole localfs
data-plane owner. To use S3:
- Fill in endpoint, region, bucket, access key, secret, TLS, and path-style
settings in
.env. - Set
TELESRV_BLOB_BACKEND=s3. - In an isolated environment, validate bucket permissions, range reads, multipart/upload-part garbage collection, and capacity alerts.
- Follow the offline migration constraints in the configuration reference. Existing data is not moved by changing an environment variable, and reads or writes do not fall back to the other backend.
Compose does not bundle MinIO by default, so a development object store is not mistaken for production-grade highly available S3. Use a separate override for a self-contained MinIO test environment, including bucket initialization, a persistent volume, and health checks.
Before handing off a deployment, run at least:
docker compose --project-directory deploy/docker --env-file deploy/docker/.env -f deploy/docker/compose.yaml config --quiet
docker compose --project-directory deploy/docker -f deploy/docker/compose.yaml up -d --no-build --wait
docker compose --project-directory deploy/docker -f deploy/docker/compose.yaml exec edge id -u
docker compose --project-directory deploy/docker -f deploy/docker/compose.yaml psid -uis10001; writes to the root filesystem fail, while declared data volumes remain writable.- The host exposes only the expected
2398/tcp,12399/udp,12400/udp,12500-12999/udp,2400/tcp, and the2401/tcpbinding selected in.env. - Stopping Core, File, or Egress makes Edge
unhealthy; affected business requests fail closed and do not execute in-process. Health recovers when the backend returns. - After
downwithout-vfollowed byup, the RSA fingerprint, schema, accounts, and media blobs are unchanged. - Cover online push/ACK, offline difference, Edge reconnect, Egress crash/reclaim, and SFU UDP media.
- Compose reports a required variable while parsing:
.envhas a missing value, or the command did not use the correct--project-directoryand--env-file. - The entrypoint reports a placeholder: a
CHANGEMEvalue or empty internal token remains. - File is unhealthy: inspect PostgreSQL migration state, backend kind, free
disk space, and
file_datapermissions. - Core is unhealthy: inspect FileData health/GetInfo, PostgreSQL, Redis, and the CoreExec, group-control, and public-link listeners. SFU is an independent owner, not a local Core fallback.
- Edge is unhealthy: inspect the local 2398 listener, then authenticated health for CoreExec, FileData, and Egress ACK.
- A group call joins but media fails: verify
TELESRV_ADVERTISE_IPand NAT/firewall forwarding for12399/udp. - A private call cannot use relay: confirm Core logs
turn listening, then verifyTELESRV_TURN_ADVERTISE_IPand 1:1 NAT/firewall forwarding for12400/udpand the complete12500-12999/udprange. - OBS cannot connect to RTMP: confirm Core logs
live stream rtmp ingest listening, then verifyTELESRV_LIVESTREAM_RTMP_URLand2400/tcpforwarding. The client live-stream settings page generates the stream key. - The client RSA fingerprint does not match: export the public key from the
current
edge_state; an old volume may preserve a different identity. A fresh default test volume exactly matchesdeploy/docker/assets/test-server-rsa.pub.