message-sync is a self-hosted, privacy-first service that synchronizes configured WhatsApp groups, Discord channels, and Telegram groups or supergroups. Multiple accounts and bots participate in alias-based sync sets while a transport-neutral canonical router owns message identity, fan-out, and lifecycle behavior without storing PII/PHI.
The release workflow publishes the same multi-architecture message-sync image to:
docker.io/vm75/message-syncghcr.io/vm75/message-syncaggregate-response, /poll-results). Encrypted poll questions/options survive server restarts while remaining outside the PII-free routing database.!local, #local, //, [local]) to suppress private or internal messages before transmission or media loading.sync.db) and application logs store zero message content, credentials, phone numbers, or user IDs; actor identities are HMAC-derived from IDENTITY_SECRET. Optional per-sync-set one-way Discord anonymization (anonymizeToDiscord) transforms sender names, phone numbers, and structured mentions into deterministic pseudonyms (e.g. Silent Falcon Q7M) exclusively for Discord destinations without storing PII or mapping tables.awaiting_replay), and real-time delivery health monitoring.0600 control.db supporting Admin and Operator roles, bcrypt passwords, HMAC session tokens, session revocation, one-time invite tokens, audit logging, and AES-256-GCM encrypted bot tokens.See the README for detailed product setup and provider requirements. This page is limited to image behavior, runtime configuration, and container deployment.
vX.Y.Z must exactly match VERSION=X.Y.Z in the tagged commit.latest; pre-releases such as v0.3.0-rc.1 never move latest.linux/amd64 and linux/arm64.Development builds use the development version string and are not published by the release workflow. Exact release tags/images are never rebuilt from another commit; fix a bad release with a new version.
Create a private environment file and data directory:
mkdir -p data
printf 'IDENTITY_SECRET=%s\nDATA_DIR=/data\nPORT=8080\nLOG_LEVEL=info\n' "$(openssl rand -hex 32)" > .env
chmod 600 .env
Run the current published release:
docker pull docker.io/vm75/message-sync:latest
docker run -d \
--name message-sync \
--read-only \
--cap-drop ALL \
--security-opt no-new-privileges:true \
--tmpfs /tmp:rw,noexec,nosuid,nodev,size=64m \
--env-file .env \
-p 8080:8080 \
-v "$PWD/data:/data" \
docker.io/vm75/message-sync:latest
On SELinux systems, add :Z to the /data bind mount. The image already runs as the non-root message-sync user (UID/GID 1000); ensure the mounted directory is writable by that user in your container runtime's user namespace.
Open http://localhost:8080, create the first administrator, then add connections, endpoints, and sync sets in the Web UI.
The repository's compose.yml builds message-sync:dev locally. To use the published image, remove its build block and set:
services:
message-sync:
image: docker.io/vm75/message-sync:latest
Keep the repository's read-only root filesystem, dropped capabilities, /tmp tmpfs, environment, port, and /data volume settings. Then run:
docker compose up -d
Podman users can use podman compose up -d with the same file.
The image entry point is /usr/local/bin/message-sync; its default command is run.
| Variable | Required | Default | Purpose |
|---|---|---|---|
IDENTITY_SECRET | yes | none | Identity-HMAC and credential-encryption root secret; minimum 32 bytes. |
DATA_DIR | no | /data | Persistent state and private evidence directory. |
PORT | no | 8080 | HTTP listen port when API_ADDR is unset. |
API_ADDR | no | derived from PORT | Complete HTTP listen address. |
LOG_LEVEL | no | info | debug, info, warn, or error. |
WHATSAPP_DEVICE_NAME | no | message-sync | Companion device name shown in WhatsApp Linked Devices. |
VERIFICATION_MAIL_API_KEY | no | none | Enables Resend-compatible email delivery for experimental membership verification with VERIFICATION_MAIL_FROM. |
VERIFICATION_MAIL_FROM | no | none | Sender used by optional experimental membership email delivery. |
VERIFICATION_PUBLIC_BASE_URL | no | request origin | External base URL used in experimental membership links behind a proxy. |
OPENROUTER_API_KEY | no | none | Enables advisory image evidence analysis only when training is explicitly disabled. |
OPENROUTER_ALLOW_TRAINING | no | unset | Must equal false for OpenRouter analysis to run. |
OPENROUTER_MODEL | no | openrouter/free | Model used by optional advisory analysis. |
Discord and Telegram bot tokens are pasted into the authenticated Web UI and encrypted in control.db; do not place them in container environment variables.
The service exposes HTTP on the configured port and requires one persistent writable mount at /data. That mount contains:
sync.db — PII-free routing/configuration state;control.db — sensitive accounts, sessions, encrypted credentials, audit, and experimental membership state;whatsapp/<connection-id>.db — isolated sensitive whatsmeow protocol state;membership-evidence/ — short-lived private membership evidence when the experimental verification feature is used.Do not publish, inspect as application data, or expose these files through another service. Back up /data before upgrades; the project does not have a general versioned migration framework, though schema changes may use narrowly scoped idempotent initialization migrations with explicit upgrade tests.
The final image contains the compiled binary on Alpine and runs as UID/GID 1000. It needs no privileged mode, host networking, host PID namespace, or added Linux capabilities. /data is the only persistent writable location; use a read-only root filesystem and the bounded /tmp tmpfs shown above.
docker run --rm docker.io/vm75/message-sync:latest version
docker run --rm \
--env-file .env \
-v "$PWD/data:/data" \
docker.io/vm75/message-sync:latest validate-config
validate-config requires an existing sync.db with a complete valid configuration (at least two endpoints in one or more sync sets).
.github/workflows/release-images.yml runs only for pushed release tags matching v*.*.*. A normal push to main, including a commit that changes VERSION, does not publish anything. The workflow validates the tag against VERSION, runs go test ./... and go vet ./..., verifies the release-mode binary version, and only then publishes the multi-architecture images.
Stable versions publish both the exact version and latest to Docker Hub and GHCR. Pre-release versions publish only their exact version. The workflow refuses to overwrite an existing exact-version image, verifies the published GHCR image reports the same version, and creates the matching GitHub Release only after publishing succeeds.
The GitHub repository must provide DOCKERHUB_USERNAME and DOCKERHUB_TOKEN secrets. GHCR authentication uses the workflow GITHUB_TOKEN; the publish job receives only contents: read and packages: write, while the final GitHub Release job receives contents: write. No registry credentials belong in the repository.
Maintainers should follow RELEASING.md; do not create release tags by editing this document or by rebuilding an old exact version.
Content type
Image
Digest
sha256:45349e433…
Size
14.3 MB
Last updated
3 days ago
docker pull vm75/message-sync