3.2 KiB
3.2 KiB
| summary | tags | scope | date | last_updated | |||
|---|---|---|---|---|---|---|---|
| Deployment/testing gotchas for the self-hosted apprise-api Docker container — correct image name (caronc/apprise, NOT caronc/apprise-api), config-dir ownership (uid 1000 workers), stateful config format, container-localhost delivery-test trap, and the tailnet-bind pattern used on ovh-vps. |
|
global | 2026-07-10 | 2026-07-13 |
apprise-api self-hosted deployment gotchas
Learned deploying caronc/apprise (apprise-api) via docker compose on ovh-vps (cc-os ADR-024, 2026-07-10).
Image name
- The Docker Hub image is
docker.io/caronc/apprise—caronc/apprise-apidoes not exist (404 on the Hub API), despite the project being named apprise-api on GitHub.
Container-localhost delivery-test trap
- Inside the container, notification target URLs like
json://localhost:1resolve to the container's own network namespace, not the host — stateless delivery to them always fails with HTTP 424 ("One or more notifications could not be sent"). - Integration tests against a throwaway container therefore need either an externally reachable target (opt-in via env var) or an error-round-trip assertion (expect the 424 to surface); config
addcalls are safe with unreachable URLs since they store without delivering. - On Fedora with SELinux,
docker runhits EACCES on this setup — use rootless podman.
Config-dir ownership trap
docker exec apprise idreports root, but the gunicorn workers actually run as uid 1000 — the container chowns the mounted/configvolume to1000:1000on first start.- A stateful config file owned by another uid with mode
600produces a misleading 500:django: NOTIFY - I/O error accessing configuration using KEY: <key>. Fix:chown 1000:1000the{key}.cfgfile (keep600). - Corollary: after the container starts, the host user may lose write access to
./config(it was chowned to 1000) —sudo chownback or write files as the right uid.
Stateful config essentials
APPRISE_STATEFUL_MODE=simple→ config is a plain text file/config/{KEY}.cfg, one target URL per line, optionaltag=prefix (pushover=pover://user@token). Untagged lines match notify calls with no tag.APPRISE_CONFIG_LOCK=yesdisables config changes via the API — config is file-managed only; notify still works.- A long random KEY (
openssl rand -hex 16) acts as the only auth token; treat the.cfgfilename/key as a secret. - Notify:
POST /notify/{key}form-encodedbody+title(+ optionaltag,type) → 200 on delivery.
Network pattern (no auth built in)
apprise-api has no authentication — don't expose it publicly bare. Pattern used: bind the container port to the tailscale IP + localhost only (100.100.43.95:8901:8000, 127.0.0.1:8901:8000), so every tailnet machine can notify with no public exposure, no DNS, no reverse proxy. Server env (base URL + key) lives in ~/services/apprise/apprise.env (600) on ovh-vps.
Related: vault-backlog-pilot-plan (ADR-024 context); Ruby client gem apprise-api (module Apprise, ships apprise-notify CLI) at ~/dev/ruby-gems/apprise-api — the earlier minimal apprise-client gem was retired and deleted 2026-07-13.