SecondBrain/reference/apprise-api-selfhosted-gotc...

44 lines
3.2 KiB
Markdown
Raw Normal View History

2026-07-10 14:52:23 +00:00
---
2026-07-13 20:01:53 +00:00
summary: 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.
2026-07-10 14:52:23 +00:00
tags:
- type/reference
- tool/apprise
- domain/notifications
scope: global
date: 2026-07-10
2026-07-13 20:01:53 +00:00
last_updated: 2026-07-13
2026-07-10 14:52:23 +00:00
---
# 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).
2026-07-13 20:14:28 +00:00
## Image name
- The Docker Hub image is **`docker.io/caronc/apprise`** — `caronc/apprise-api` does **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:1` resolve 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 `add` calls are safe with unreachable URLs since they store without delivering.
- On Fedora with SELinux, `docker run` hits EACCES on this setup — use rootless **podman**.
2026-07-10 14:52:23 +00:00
## Config-dir ownership trap
- `docker exec apprise id` reports **root**, but the gunicorn workers actually run as **uid 1000** — the container chowns the mounted `/config` volume to `1000:1000` on first start.
- A stateful config file owned by another uid with mode `600` produces a misleading 500: `django: NOTIFY - I/O error accessing configuration using KEY: <key>`. Fix: `chown 1000:1000` the `{key}.cfg` file (keep `600`).
- Corollary: after the container starts, the host user may lose write access to `./config` (it was chowned to 1000) — `sudo chown` back 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, optional `tag=` prefix (`pushover=pover://user@token`). Untagged lines match notify calls with no tag.
- `APPRISE_CONFIG_LOCK=yes` disables 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 `.cfg` filename/key as a secret.
- Notify: `POST /notify/{key}` form-encoded `body` + `title` (+ optional `tag`, `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.
2026-07-13 20:14:28 +00:00
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.