Notification channels (ntfy, Gotify, webhook)
Since Containly 2.7.1 notifications also go to ntfy, Gotify or any webhook. Free for normal notifications, Pro inside workflows. Setup, test button, filters, webhook format with HMAC signature, security rules and delivery log explained.
Since Containly 2.7.1 notifications are not limited to e-mail and the bell in the app. Under Settings > Notifications > Channels you add channels that receive the same notifications: ntfy (push to your phone through ntfy.sh or your own ntfy server), Gotify (push through your own Gotify server) and a generic webhook (JSON to any URL, for example Home Assistant, n8n or your own script).
Since Containly 2.7.2 there are also Discord, Telegram, Slack and Pushover channels and web push to your phone or browser, see Push notifications and Discord, Telegram, Slack, Pushover.
Channels for normal notifications are Free. Workflows can also notify saved channels; like everything in Workflows this needs Pro. Only admins see and change channels.
Add a channel
- Open Settings > Notifications. The Channels card is below the e-mail settings.
- Click Add channel and pick the type: ntfy, Gotify or Webhook.
- Fill in the fields:
| Type | Fields |
|---|---|
| ntfy | Server URL (for example https://ntfy.sh or http://192.168.1.20:8080), topic, sign-in (none, access token, or user and password), priority per severity (1 to 5), optional click URL |
| Gotify | Server URL, app token (create an application under Apps in Gotify and copy its token), priority per severity (0 to 10) |
| Webhook | URL, optional signature (signing secret, Generate creates a random one), up to 10 custom headers (for example Authorization) |
- Choose the minimum severity (Info, Warning or Critical), the events (all events that are switched on under Events, or a selection) and the message language.
- Click Send test. Containly sends one real message with the entered settings and saves nothing. The result says what went wrong, for example "Sign-in refused (HTTP 401)" or "loopback addresses are never allowed". Change a field and the result disappears, so it always belongs to the current settings.
- Click Save.
On ntfy.sh anyone who knows a topic can read it. Pick a long random topic name or protect it with an access token.
Which notifications go to a channel
A channel receives every notification type that is switched on under Settings > Notifications > Events (offline hosts, stopped, unhealthy or OOM-killed containers, restart loops, CPU, memory and disk, updates, critical CVEs, backups), filtered by the channel's minimum severity and optional event selection. The 30 minute cooldown per event and object applies like for e-mail.
Workflow messages never reach a channel by themselves: in a workflow's Notify step you tick the channels it should use next to In-app and E-mail (Pro). Without Pro, Containly refuses those workflow messages on the server and logs them as "Workflows need Pro" in the channel's delivery log.
Status and delivery log
Each channel in the list shows its status: Working, Error (the last delivery failed), Failing (10 failed deliveries in a row, Containly keeps trying), Not used yet or Off. Send test in the list tests the saved channel. The three-dot menu has Edit, Delivery log and Delete.
The delivery log shows the last 20 attempts with time, event, result, HTTP status and the number of attempts. Message content and secrets are never stored there.
Limits and retries
- At most 20 messages per minute per channel. More messages are held back and summarised in one message ("12 more notifications were held back").
- Temporary errors (HTTP 429, server errors 5xx, timeouts, connection errors) are retried 3 times after 1, 2 and 4 seconds; a
Retry-Afterfrom the server is honoured up to 30 seconds. Other errors (for example a wrong token) are not retried. - Each request has a 10 second timeout; redirects are not followed.
Security
- Secrets (tokens, passwords, signing secret, header values) are stored encrypted (AES-256-GCM with the instance key) and are never shown again, never returned by the API and never written to logs or the audit log. Leave a secret field empty to keep the stored value.
- If you change the destination (scheme, host or port) of a saved channel, you must enter its secrets again. This way a stored token can never be sent to another server, not even by a test.
- Outbound policy: Containly never sends to loopback addresses (
127.0.0.1,localhost), link-local addresses (169.254.x.x), cloud metadata addresses or to itself. Every address a host name resolves to is checked and the connection uses exactly that address. - Your own network: addresses such as
192.168.x.x,10.x.x.xor172.16.x.xto172.31.x.xare allowed by default, because ntfy and Gotify usually run there. Switch on Block private targets in the Channels card to refuse them too. The server settingCONTAINLY_CHANNELS_BLOCK_PRIVATE=1forces this on. - Plain
http://is only allowed to addresses in your own network; public receivers needhttps://. - To reach a receiver on the same Docker host, use its LAN address or put it in a Docker network shared with Containly and use the container name;
localhostinside the Containly container is Containly itself.
Webhook format
Containly sends a POST with Content-Type: application/json. The body (version 1):
{
"version": 1,
"id": "6f1c2a9e-...",
"type": "container.exited",
"severity": "warning",
"title": "Containly · Container stopped: web",
"message": "The container stopped with exit code 3.\nContainer: web\nHost: homelab\nExit code: 3",
"target": "web",
"link": "https://containly.example.com/containers",
"at": "2026-10-19T08:00:00.000Z",
"test": false,
"source": "notification"
}
typeis the notification type (for exampleendpoint.offline,backup.failed,workflow.alert),channel.testfor a test andchannel.suppressedfor the rate limit summary (with an extra fieldsuppressed, the number of held back messages).sourceisnotification,workflow,testorsummary.linkneedsCONTAINLY_PUBLIC_URL; without it the value isnull.- Headers:
X-Containly-Event(the type) andX-Containly-Delivery(the id) always; with a signing secret alsoX-Containly-Timestamp(unix seconds) andX-Containly-Signature: sha256=<hex>.
Check the signature in your receiver: compute HMAC-SHA256 with your signing secret over <timestamp>.<raw body> (the timestamp header, a dot, then the body exactly as received) and compare it with the hex value after sha256= in constant time. Reject old timestamps (for example older than 5 minutes) to stop replays. Python example:
import hashlib, hmac, time
def verify(secret: bytes, headers, raw_body: bytes) -> bool:
ts = headers["X-Containly-Timestamp"]
if abs(time.time() - int(ts)) > 300:
return False
want = "sha256=" + hmac.new(secret, ts.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(want, headers["X-Containly-Signature"])
Troubleshooting
| Message | What to do |
|---|---|
| Refused: loopback addresses are never allowed | Use the LAN address of the host or the container name in a shared Docker network instead of localhost |
| Refused: private network addresses are blocked | Block private targets is on; switch it off or use a public https:// address |
| Public addresses need https:// | Use https:// for receivers on the internet |
| Host name not found | Check the spelling and the DNS of the Containly container |
| Sign-in refused (HTTP 401) / Access denied (HTTP 403) | Check token, password or the Gotify app token |
| TLS failed | The receiver's certificate is not trusted or does not match the host name |
| The destination changed: enter the value again | You changed the server; enter the token or password again |