ntfy
Self-hosted push notification service using simple HTTP pub-sub. Publish notifications from any script, CI pipeline,
or monitoring system with a single curl command. Subscribe on Android, iOS, or the web. No signup, no cost, no
third-party service — all traffic stays on your infrastructure.
ntfy.authDefaultAccess defaults to read-write, meaning any unauthenticated user can publish and subscribe to any
topic. If you expose ntfy to the internet, set authDefaultAccess: deny-all and configure authentication via
ntfy.extraConfig to prevent unauthorized access and notification spam.
Key Features
- HTTP pub-sub — publish via
PUT/POST, subscribe viaGETor WebSocket - Mobile and web clients — Android, iOS apps and progressive web app
- Access control — per-topic user and permission management
- Attachment support — configurable file attachment size and expiry limits
- Behind-proxy mode — correct client IP identification via
X-Forwarded-For - Prometheus metrics — optional
/metricsendpoint with ServiceMonitor support - Persistent storage — PVC-backed SQLite cache and authentication databases
- Gateway API — optional HTTPRoute rendering for modern ingress controllers
Installation
HTTPS repository:
helm repo add helmforge https://repo.helmforge.dev
helm repo update
helm install ntfy helmforge/ntfy
OCI registry:
helm install ntfy oci://ghcr.io/helmforgedev/helm/ntfy
Deployment Examples
# values.yaml — ntfy with ingress, behind Traefik
ntfy:
baseUrl: 'https://ntfy.example.com'
behindProxy: true
persistence:
enabled: true
size: 2Gi
ingress:
enabled: true
ingressClassName: traefik
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: ntfy.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: ntfy-tls
hosts:
- ntfy.example.comAfter deploying, send your first notification:
curl -d "Hello from Kubernetes!" https://ntfy.example.com/my-topic# values.yaml — ntfy with authentication enabled (deny anonymous access)
ntfy:
baseUrl: 'https://ntfy.example.com'
behindProxy: true
authDefaultAccess: deny-all
extraConfig: |
auth-file: /var/cache/ntfy/user.db
persistence:
enabled: true
size: 2Gi
ingress:
enabled: true
ingressClassName: traefik
hosts:
- host: ntfy.example.com
paths:
- path: /
pathType: PrefixAfter deploying, create users via the ntfy CLI inside the pod:
# Create an admin user
kubectl exec -it deployment/ntfy -- ntfy user add --role=admin admin
# Publish with authentication
curl -u admin:password -d "Secure notification" https://ntfy.example.com/my-topic# values.yaml — ntfy with file attachment support
ntfy:
baseUrl: 'https://ntfy.example.com'
behindProxy: true
attachmentTotalSizeLimit: '200M'
attachmentFileSizeLimit: '20M'
attachmentExpiryDuration: '24h'
persistence:
enabled: true
# Increase PVC size to accommodate attachments
size: 10Gi
ingress:
enabled: true
ingressClassName: traefik
hosts:
- host: ntfy.example.com
paths:
- path: /
pathType: Prefix# values.yaml — ntfy with Prometheus metrics
ntfy:
baseUrl: 'https://ntfy.example.com'
enableMetrics: true
# Expose metrics on a separate port to avoid routing /metrics via Ingress
metricsListenHttp: ':9090'
persistence:
enabled: true
size: 2Gi
metrics:
serviceMonitor:
enabled: true
interval: 30s
ingress:
enabled: true
ingressClassName: traefik
hosts:
- host: ntfy.example.com
paths:
- path: /
pathType: Prefix# values.yaml — ntfy exposed with Gateway API
ntfy:
baseUrl: 'https://ntfy.example.com'
behindProxy: true
persistence:
enabled: true
size: 2Gi
gateway:
enabled: true
parentRefs:
- name: public
namespace: gateway-system
hostnames:
- ntfy.example.comConfiguration Reference
Core
| Parameter | Type | Default | Description |
|---|---|---|---|
nameOverride |
string | "" |
Override the chart name. |
fullnameOverride |
string | "" |
Override the full release name. |
commonLabels |
object | {} |
Extra labels added to all resources. |
Image
| Parameter | Type | Default | Description |
|---|---|---|---|
image.repository |
string | docker.io/binwiederhier/ntfy |
ntfy container image. |
image.tag |
string | "v2.24.0" |
Image tag. |
image.pullPolicy |
string | IfNotPresent |
Image pull policy. |
imagePullSecrets |
array | [] |
Pull secrets for private registries. |
ntfy Configuration
| Parameter | Type | Default | Description |
|---|---|---|---|
ntfy.baseUrl |
string | "" |
Public base URL of the instance (e.g. https://ntfy.example.com). |
ntfy.authDefaultAccess |
string | read-write |
Default access for unauthenticated users: read-write, read-only, deny-all. |
ntfy.behindProxy |
boolean | true |
Trust X-Forwarded-For headers for correct client IP and rate limiting. |
ntfy.enableMetrics |
boolean | false |
Enable Prometheus metrics at /metrics. |
ntfy.metricsListenHttp |
string | "" |
Separate listen address for the metrics endpoint (e.g. :9090). |
ntfy.attachmentTotalSizeLimit |
string | "" |
Total attachment storage limit per visitor (e.g. 100M). |
ntfy.attachmentFileSizeLimit |
string | "" |
Maximum size per attachment file (e.g. 15M). |
ntfy.attachmentExpiryDuration |
string | "" |
How long attachments are retained (e.g. 3h). |
ntfy.extraConfig |
string | "" |
Raw server.yml configuration appended to the generated ConfigMap. |
ntfy.extraEnv |
array | [] |
Extra environment variables injected into the container. |
The ntfy.extraConfig field appends raw server.yml lines to the generated ConfigMap. Use it for any ntfy server
option not exposed as a dedicated value, such as authentication providers, Firebase credentials, or per-topic limits.
See the ntfy server configuration reference for all available options.
When running behind a Kubernetes Ingress controller, all requests arrive from the Ingress pod IP unless
X-Forwarded-For is trusted. Keep ntfy.behindProxy: true (the default) so rate limits and client IP logging work
correctly.
Persistence
ntfy stores its SQLite cache database and authentication database in /var/cache/ntfy. Persistence is enabled by
default to survive pod restarts without losing message history and user accounts.
| Parameter | Type | Default | Description |
|---|---|---|---|
persistence.enabled |
boolean | true |
Enable a PVC for /var/cache/ntfy. |
persistence.size |
string | 2Gi |
PVC size. Increase if using attachments. |
persistence.storageClass |
string | "" |
StorageClass for the PVC. |
persistence.accessModes |
array | ["ReadWriteOnce"] |
PVC access modes. |
persistence.existingClaim |
string | "" |
Use an existing PVC instead of creating one. |
Service
| Parameter | Type | Default | Description |
|---|---|---|---|
service.type |
string | ClusterIP |
Kubernetes service type. |
service.port |
integer | 80 |
Service port exposed to the cluster. |
service.annotations |
object | {} |
Annotations for the Service. |
Ingress
| Parameter | Type | Default | Description |
|---|---|---|---|
ingress.enabled |
boolean | false |
Enable an Ingress resource. |
ingress.ingressClassName |
string | traefik |
Ingress class name. |
ingress.annotations |
object | {} |
Annotations for the Ingress (e.g. cert-manager). |
ingress.hosts |
array | [] |
Ingress host and path rules. |
ingress.tls |
array | [] |
TLS configuration (secret name and hosts). |
Gateway API
| Parameter | Type | Default | Description |
|---|---|---|---|
gateway.enabled |
boolean | false |
Render an HTTPRoute. Requires Gateway API CRDs and a compatible Gateway. |
gateway.annotations |
object | {} |
Annotations for the HTTPRoute. |
gateway.parentRefs |
array | [] |
Gateway parent references. Required when gateway.enabled=true. |
gateway.hostnames |
array | [] |
Hostnames matched by the HTTPRoute. |
gateway.path |
string | / |
Path value matched by the HTTPRoute. |
gateway.pathType |
string | PathPrefix |
Path match type. |
Probes
Probes use the /v1/health endpoint.
| Parameter | Type | Default | Description |
|---|---|---|---|
probes.startup.enabled |
boolean | true |
Enable startup probe. |
probes.startup.initialDelaySeconds |
integer | 5 |
Startup probe initial delay. |
probes.startup.periodSeconds |
integer | 5 |
Startup probe period. |
probes.startup.timeoutSeconds |
integer | 3 |
Startup probe timeout. |
probes.startup.failureThreshold |
integer | 30 |
Startup probe failure threshold. |
probes.liveness.enabled |
boolean | true |
Enable liveness probe. |
probes.liveness.initialDelaySeconds |
integer | 0 |
Liveness probe initial delay. |
probes.liveness.periodSeconds |
integer | 15 |
Liveness probe period. |
probes.liveness.timeoutSeconds |
integer | 5 |
Liveness probe timeout. |
probes.liveness.failureThreshold |
integer | 3 |
Liveness probe failure threshold. |
probes.readiness.enabled |
boolean | true |
Enable readiness probe. |
probes.readiness.initialDelaySeconds |
integer | 0 |
Readiness probe initial delay. |
probes.readiness.periodSeconds |
integer | 10 |
Readiness probe period. |
probes.readiness.timeoutSeconds |
integer | 5 |
Readiness probe timeout. |
probes.readiness.failureThreshold |
integer | 3 |
Readiness probe failure threshold. |
Metrics
| Parameter | Type | Default | Description |
|---|---|---|---|
metrics.serviceMonitor.enabled |
boolean | false |
Create a ServiceMonitor for Prometheus Operator. |
metrics.serviceMonitor.interval |
string | 30s |
Prometheus scrape interval. |
metrics.serviceMonitor.labels |
object | {} |
Extra labels applied to the ServiceMonitor. |
Resources and Security
| Parameter | Type | Default | Description |
|---|---|---|---|
resources |
object | {} |
CPU and memory requests and limits. |
podSecurityContext |
object | {} |
Pod-level security context. |
securityContext |
object | {} |
Container-level security context. |
Service Account
| Parameter | Type | Default | Description |
|---|---|---|---|
serviceAccount.create |
boolean | false |
Create a dedicated ServiceAccount. |
serviceAccount.name |
string | "" |
Override the ServiceAccount name. |
serviceAccount.annotations |
object | {} |
Annotations for the ServiceAccount. |
Scheduling
| Parameter | Type | Default | Description |
|---|---|---|---|
nodeSelector |
object | {} |
Node selector for scheduling. |
tolerations |
array | [] |
Tolerations for scheduling. |
affinity |
object | {} |
Affinity rules. |
topologySpreadConstraints |
array | [] |
Topology spread constraints. |
priorityClassName |
string | "" |
PriorityClass for the pod. |
terminationGracePeriodSeconds |
integer | 30 |
Termination grace period. |
podLabels |
object | {} |
Extra labels for the pod. |
podAnnotations |
object | {} |
Extra annotations for the pod. |
Extra
| Parameter | Type | Default | Description |
|---|---|---|---|
extraVolumes |
array | [] |
Extra volumes to attach to the pod. |
extraVolumeMounts |
array | [] |
Extra volume mounts for the container. |
extraManifests |
array | [] |
Extra Kubernetes manifests deployed alongside the chart. |
Common Issues
If rate limiting is not working or logs show all requests from the same IP (the Ingress pod), verify that
ntfy.behindProxy: true is set (it is the default) and that your Ingress controller is forwarding the
X-Forwarded-For header.
After deploying, verify your setup with a quick curl command:
curl -d "Test notification from Kubernetes" https://ntfy.example.com/test-topicSubscribe to https://ntfy.example.com/test-topic in the ntfy app to receive it.