Configurable, security-hardened Docker container for signal-cli daemon. Designed for Hermes Agent integration and other automation use cases.
# 1. Clone repo
git clone https://github.com/libre-7/signal-cli-gateway.git
cd signal-cli-gateway
# 2. (Optional) Build the image locally
docker build -t signal-cli-gateway .
# 3. Link your phone (one-time setup)
mkdir -p signal-cli-data
docker run --rm -it \
-v "$(pwd)/signal-cli-data:/opt/signal-cli-data" \
-e DEVICE_NAME=HermesAgent \
signal-cli-gateway \
bash /scripts/link-account.sh
# Scan the QR code from Signal → Settings → Linked Devices
# 4. Configure Hermes
# In your Hermes .env:
# SIGNAL_HTTP_URL=http://127.0.0.1:8880
# SIGNAL_ACCOUNT=+123****7890
# (Hermes connects to the proxy — IP allowlist bypasses auth)
# 5. Edit SIGNAL_ACCOUNT in compose.yaml, then start
docker compose up -d| Mode | Env Var | signal-cli | Exposed | Auth | Network | Use Case |
|---|---|---|---|---|---|---|
| Loopback | loopback |
127.0.0.1:8080 | ❌ | None | Host | Hermes on host networking, trusted LAN — ✅ Shipped default |
| Loopback + Proxy | loopback-proxy |
127.0.0.1:8080 | 0.0.0.0:8880 (proxy, ipFilter: local-only) | Bearer + IP allowlist + ipFilter | Host | ✅ Recommended for production |
| Exposed + Proxy | exposed-proxy |
127.0.0.1:8080 | 0.0.0.0:8880 (proxy) | Bearer + IP allowlist | Bridge or Host | Multi-host, Kubernetes, cloud |
| UNIX Socket | unix |
/var/run/signal-cli/socket |
Socat bridge 127.0.0.1:8080 | File perms | Host | Maximum process isolation |
Network note: Modes that bind to
127.0.0.1(loopback, loopback-proxy, unix) require--network hostbecause127.0.0.1inside a bridge network is unreachable from outside the container.exposed-proxyworks on bridge — the proxy binds0.0.0.0:8880, so port mapping (-p 8880:8880) works, and signal-cli stays on loopback internally.
compose.yaml and the Unraid deploy script default to loopback: signal-cli
binds to 127.0.0.1 and nothing else is exposed, which is safe on a trusted
single-host setup with host networking.
- signal-cli is never exposed — binds to 127.0.0.1, unreachable from other containers
- No ports opened at all in loopback mode — smallest possible attack surface
- Upgrade path: set
SECURITY_MODE=loopback-proxywhen other containers or remote clients need access — then every proxied request requires a Bearer token or Basic Auth, and an ipFilter restricts the proxy to local callers - IP allowlist for Hermes — trusted IPs bypass auth so Hermes'
signal.pyadapter works unpatched - Auto-generated random token — if you don't set
SECURITY_PROXY_TOKEN, one is generated and logged at startup
loopback mode:
signal-cli daemon → 127.0.0.1:8080 (loopback, TCP)
loopback-proxy mode:
signal-cli daemon → 127.0.0.1:8080 (loopback, unreachable)
↑ (proxied, secured)
secured-signal-api → 0.0.0.0:8880 (Bearer auth + IP allowlist + ipFilter: local-only)
↑
Hermes → 127.0.0.1:8880 (IP is trusted, no auth needed)
exposed-proxy mode:
signal-cli daemon → 127.0.0.1:8080 (loopback, unreachable)
↑ (proxied, secured)
secured-signal-api → 0.0.0.0:8880 (Bearer auth + IP allowlist)
↑
Any client → <host>:8880 (Bearer token required)
unix mode:
signal-cli daemon → /var/run/signal-cli/socket
↑ (socat bridge)
socat → 127.0.0.1:8080 (loopback, TCP)
Copy .env.example to .env before docker compose up. Compose reads .env
in two distinct ways, and this repo uses both:
- Variable interpolation —
${SIGNAL_ACCOUNT:?set SIGNAL_ACCOUNT in .env}incompose.yaml. The gateway refuses to start if.envis missing or the variable unset (fail-fast instead of a silently empty account). env_file:— passes all.enventries into the container so the entrypoint seesSECURITY_MODE,PROXY_PORT, etc.
Note that compose does not auto-inject a .env file into an
environment: list; the mechanisms above are how it is wired in. The
healthcheck ports use ${PROXY_PORT:-8880} / ${SIGNAL_CLI_PORT:-8080}
interpolation, so they follow custom ports set in .env.
| Variable | Default | Description |
|---|---|---|
| Required | ||
SIGNAL_ACCOUNT |
— | Your Signal number in E.164 format (e.g. +123****7890) |
| Security | ||
SECURITY_MODE |
loopback |
One of: loopback, loopback-proxy, exposed-proxy, unix |
SECURITY_PROXY_TOKEN |
auto-generated | Bearer token for proxy authentication |
SECURITY_PROXY_ALLOWED_IPS |
127.0.0.1 |
CIDR ranges that bypass proxy auth |
| Ports | ||
SIGNAL_CLI_PORT |
8080 |
signal-cli daemon port |
PROXY_PORT |
8880 |
secured-signal-api proxy port |
| signal-cli options | ||
SIGNAL_CLI_TRUST_NEW_IDENTITIES |
on-first-use |
on-first-use, always, never |
| Account linking | ||
DEVICE_NAME |
SignalGateway |
Name shown in Signal's linked device list |
Once the gateway is running, add these to your Hermes .env:
# If gateway uses host networking:
SIGNAL_HTTP_URL=http://127.0.0.1:8880
SIGNAL_ACCOUNT=+123****7890
SIGNAL_HOME_CHANNEL=+123****7890
# Or if gateway is a separate container on a Docker network:
# SIGNAL_HTTP_URL=http://signal-cli-gateway:8880Hermes connects to the proxy on port 8880. If Hermes' container IP is in the
SECURITY_PROXY_ALLOWED_IPS list, no Bearer token is needed — the proxy trusts
the connection source. All other callers must authenticate.
No Hermes adapter patches needed. The built-in gateway/platforms/signal.py
works as-is when the IP is allowlisted.
Mount your own config.yml to /config/config.yml to override default
settings — add rate limiting, field policies, message templates, etc.
See secured-signal-api docs
for the full configuration reference.
docker run -d --name signal-cli-gateway --restart unless-stopped \
--network host \
-v "$(pwd)/signal-cli-data:/opt/signal-cli-data" \
-v "$(pwd)/custom-config.yml:/config/config.yml" \
-e SIGNAL_ACCOUNT=+123****7890 \
-e SECURITY_MODE=loopback-proxy \
signal-cli-gatewaymkdir -p /mnt/user/appdata/signal-cli-data
# Run link in interactive mode
docker run --rm -it \
-v /mnt/user/appdata/signal-cli-data:/opt/signal-cli-data \
-e DEVICE_NAME=HermesAgent \
signal-cli-gateway \
bash /scripts/link-account.shThis prints a URI like:
sgnl://linkdevice?uuid=XXXX&pub_key=YYYY
On a headless system, the link script prints an ANSI QR code itself (offline,
via qrencode — no third-party service is contacted). To render one manually:
echo 'sgnl://linkdevice?uuid=XXXX&pub_key=YYYY' | \
docker run --rm -i signal-cli-gateway qrencode -t ANSI256Scan from your phone: Signal → Settings → Linked Devices → +.
The Dockerfile builds secured-signal-api from source. To skip the proxy
(leaner image, no Go build needed):
docker build --target signal-cli-builder -t signal-cli-gateway:no-proxy .
# Copy from stage 1 into a final Ubuntu baseSee DESIGN.md for the full security analysis, threat model, and comparison of all approaches considered.
GNU General Public License v3.0