# signal_api Runs [signal-cli-rest-api](https://github.com/bbernhard/signal-cli-rest-api) on the `observability` host. Gatus uses it to deliver alerts over Signal. Gatus does not speak Signal — it POSTs JSON to this service, which holds the Signal identity and does the protocol work. ## It is never published, and that is not optional **This API has no authentication of any kind.** No key, no token, no basic auth. Anything that can reach the port can send messages as your identity and read your Signal. So the compose file publishes **no ports at all** and there is no Caddy vhost. Gatus reaches it over a shared docker network (`monitoring`) by service name: `http://signal-api:8080`. That is also *why* a shared network is needed rather than a published port — Gatus runs in a container, so `127.0.0.1` for Gatus is the Gatus container, not the host. The network is created by an explicit Ansible task in both this role and `gatus`, so neither stack has to be deployed before the other. ## MODE, and why `native` Upstream offers `normal`, `native`, `json-rpc` and `json-rpc-native`. The json-rpc modes keep a resident JVM daemon and upstream describes them as "increased memory". **This VPS has 464 MB of RAM**, already running Gatus and Caddy. A resident JVM is not affordable. `native` runs a precompiled GraalVM binary per request — no daemon, no resident cost — and alerts are rare enough that paying startup cost per alert is the right trade. ## Linking the device — a one-time manual step Ansible cannot scan a QR code, so this is manual. **Do not use `/v1/qrcodelink`** — it is broken in `native` mode. ### The trap `GET /v1/qrcodelink?device_name=...` returns: ```json {"error":"Couldn't create QR code: no data to encode"} ``` The linking itself is fine: running the binary directly inside the container emits a perfectly good provisioning URI. ``` $ docker exec signal-api signal-cli-native link -n gatus sgnl://linkdevice?uuid=...&pub_key=... ``` It is the REST wrapper that fails to capture that output in `native` mode. **Do not "fix" this by switching MODE to `normal` or `json-rpc`.** That puts a JVM in the path of *every alert* on a 464 MB host, permanently degrading the running system to work around a step performed once. Generate the QR yourself instead. ### The procedure **`docker exec` runs as root, but the service runs as uid 1000.** Without `--config`, signal-cli writes the linked account to `/root/.local/share/signal-cli` — the container's ephemeral layer, NOT the mounted volume. It looks like it worked (`Associated with: +34…`), `/v1/accounts` keeps returning `[]`, and the account is destroyed on the next `docker compose up`. Always pass `--config`. 1. Start the link and capture the URI. It must keep running while you scan: docker exec signal-api sh -c "rm -f /tmp/link.uri; \ nohup signal-cli-native --config /home/.local/share/signal-cli \ link -n gatus > /tmp/link.uri 2>/tmp/link.log & echo started" sleep 10 docker exec signal-api cat /tmp/link.uri Do **not** add `setsid`, and do **not** background `docker exec` itself from the host — the first stops the URI appearing, the second is killed when the Ansible task returns. The output is block-buffered because stdout is a file, so the URI appears only after several seconds; `stdbuf` does not help, as the buffering is GraalVM's, not libc's. 2. Render the QR on your own machine and scan it: qrencode -o /tmp/qr.png -s 12 -m 4 "sgnl://linkdevice?uuid=...&pub_key=..." 3. Phone: Signal → Settings → Linked devices → **+** → scan. Provisioning links expire in a couple of minutes, so generate and scan in one sitting. 4. Confirm — this must list the number, not `[]`: docker exec signal-api curl -s http://localhost:8080/v1/accounts 5. Send a test message: docker exec signal-api curl -s -X POST -H "Content-Type: application/json" \ -d '{"message":"test","number":"+34…","recipients":["+34…"]}' \ http://localhost:8080/v2/send Alerts are sent **from your own number**, so sending to yourself lands in Note to Self. If the device is ever unlinked from the phone, alerts stop silently — which is why this service is itself monitored. ### If the phone says "network error" The phone is not the problem. `chat.signal.org` resolves to AWS Global Accelerator **dualstack** addresses with the AAAA records first, this container has no IPv6 address at all, and this host's IPv6 path is broken — the same edge that returned a bogus 404 for the Go tarball. signal-cli reaches for an unreachable IPv6 address and dies with `Link request error: Connection closed!`, while the phone can only report a failed handshake. That is what `gai.conf` (mounted at `/etc/gai.conf`) fixes. If linking starts failing again, check it is still mounted and that `getent ahosts chat.signal.org` returns an IPv4 address first. ## Backups Deliberately **not** backed up. The data directory holds Signal private keys, and the recovery path is to link again from the phone — which takes a minute and does not depend on any stored artefact. Backing it up would copy a credential off the host to buy nothing. ## Verifying ```bash docker ps --filter name=signal-api docker exec signal-api curl -fsS http://localhost:8080/v1/health docker exec signal-api curl -fsS http://localhost:8080/v1/accounts docker logs signal-api --tail 50 ```