No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-19 11:08:56 +00:00
docker-compose.yml Add ntfy stack for Coolify with Postgres persistence 2026-09-02 08:30:40 +02:00
README.md Add ntfy stack for Coolify with Postgres persistence 2026-09-02 08:30:40 +02:00
renovate.json chore: Configure Renovate (#1) 2026-09-19 11:08:56 +00:00

coolify-ntfy

Self-hosted ntfy for Coolify, with messages persisted in PostgreSQL and optional outgoing SMTP.

Deploy

  1. Coolify → + New → Docker Compose Empty, point it at this repo.

  2. Deploy. Coolify generates the domain, the Postgres password, and the TLS cert.

  3. Create the first user — the stack ships closed (deny-all), so until you do this every request is rejected:

    docker exec -it <ntfy-container> ntfy user add --role=admin admin
    

    Find the container name under the service in Coolify, or docker ps | grep ntfy.

  4. Log in at the generated domain.

Then grant access per topic, e.g. read-write on alerts for everyone:

docker exec -it <ntfy-container> ntfy access '*' alerts rw

Configuration

Config is split across two places, and deliberately does not overlap. ntfy's precedence is CLI > environment > config file, so a setting present in both would have the env value silently win.

Host config file — /var/coolify/ntfy/server.yml

Almost everything lives here. The ntfy-config sidecar writes this file on the first deploy only if it does not already exist, then exits; every later deploy leaves it alone. Edit it directly on the host and restart the ntfy service to apply — your changes are not overwritten.

Setting Default Purpose
cache-duration 720h How long messages stay fetchable via ?poll=1 / ?since=. 0 disables storage entirely.
auth-default-access deny-all read-write opens the server to anonymous publish and read.
enable-login true Web UI login.
enable-signup false Self-service account creation. Leave off on a public domain.
behind-proxy true Trust Traefik's forwarded IP for rate limiting.
listen-http :80 Container listen address.
attachment-cache-dir /var/cache/ntfy/attachments Bound to /var/coolify/ntfy/attachments on the host.
attachment-total-size-limit 5G Total attachment storage cap.
attachment-file-size-limit 15M Per-file cap.
attachment-expiry-duration 3h How long uploads survive before ntfy prunes them.

ntfy takes full control of the attachment directory and deletes expired files from it — keep it dedicated to attachments and store nothing else there.

To change a default for new deployments, edit the heredoc in docker-compose.yml. To change a running instance, edit the host file.

Environment variables — Coolify UI

Only what genuinely cannot live in the config file:

Variable Why it must be env
SERVICE_FQDN_NTFY_80 Declaration that makes Coolify assign the domain and route Traefik.
NTFY_BASE_URL Comes from SERVICE_URL_NTFY; changes if you rename the deployment, so it must not be frozen into a generate-once file.
NTFY_DATABASE_URL Holds the Coolify-generated Postgres password; keeps the secret out of a plaintext host file.
SMTP_ADDR host:port of the SMTP server. Empty = email sending off.
SMTP_USER / SMTP_PASS SMTP credentials — kept out of the host file. PLAIN auth over STARTTLS only.
SMTP_FROM Sender address on outgoing mail.
TZ Container-level variable — ntfy has no config-file equivalent. Defaults to Europe/Helsinki.

Coolify generates SERVICE_PASSWORD_POSTGRES and the SERVICE_FQDN_NTFY_80 / SERVICE_URL_NTFY pair itself — don't set them by hand.

Note that ntfy's precedence is CLI > environment > config file, so any NTFY_* variable you add in the Coolify UI silently overrides the host file. That is why the two surfaces are kept disjoint.

Sending email

Set the four SMTP_* variables in Coolify's Environment Variables tab and redeploy. Only PLAIN auth over STARTTLS is supported. Then:

curl -d "backup finished" -H "X-Email: you@example.com" https://<your-domain>/alerts

Backups

The message store is the postgres service, so Coolify's scheduled database backups cover it. Add one under the Postgres service → Backups. The ntfy container holds no state and needs no backup.

Notes

  • docker-compose.yml is Coolify-only and will not run under plain docker compose — exclude_from_hc is a Coolify extension, and vanilla compose rejects it with additional properties 'exclude_from_hc' not allowed. Delete that one line to run the stack locally; it exists so Coolify does not count the exited ntfy-config sidecar against overall stack health.
  • The sidecar reuses the ntfy image rather than pulling busybox, so there is no second image to keep patched. It runs as root, so the generated server.yml is root-owned — edit it with sudo.
  • Generate-once cuts both ways. New settings added to the heredoc in docker-compose.yml do not reach an instance that already has a server.yml — the sidecar sees the file and leaves it alone. After pulling a change that adds config keys (the attachment-* block, for one), either copy the new lines into /var/coolify/ntfy/server.yml by hand, or delete the file and redeploy to regenerate it from scratch.
  • ntfy pings Postgres once at startup and exits FATAL if the database isn't up yet — it has no retry loop. depends_on handles both halves of this (condition: service_healthy for the first boot, restart: true so ntfy is relaunched whenever Postgres restarts). If you do see a database ping failed line in the logs right after a Postgres backup or upgrade, that is this race and it self-heals.
  • Attachments and web push are off. Both need extra config (attachment-cache-dir plus a volume; VAPID keys via ntfy webpush keys) and neither was asked for.
  • SQLite is the one-container alternative: drop the postgres service, swap NTFY_DATABASE_URL for NTFY_CACHE_FILE / NTFY_AUTH_FILE on a volume. You lose Coolify's managed database backups.