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
- Abuse ban-feed — opt-in weighted offender feed for fail2ban or equivalent enforcement
- 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
- PostgreSQL — external database URL loaded from a Kubernetes Secret
- External Secrets — optional credential reconciliation from provider-backed stores
- 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
Upgrade guidance
The current chart defaults to ntfy 2.27.0, with hardened message templates, verified-email login, and stable PostgreSQL
support. Existing values and the opt-in abuse ban-feed remain compatible. The
--reset-then-reuse-values flag retains an explicit image.tag from the current release; remove or update that override
if you want to adopt the chart’s v2.27.0 default:
helm upgrade ntfy helmforge/ntfy \
--reset-then-reuse-values
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 — emit abusive visitors for an external fail2ban consumer
ntfy:
banFeed:
enabled: true
file: /var/cache/ntfy/ban.log
window: 10m
threshold: 100
weights:
- '42909:10'The default file is stored on ntfy’s writable data volume. Configure an external consumer and rotate the file with copy-truncate semantics so it cannot grow without bound. The chart does not install fail2ban or modify node firewall rules.
# values.yaml — PostgreSQL URL from a provider-managed Secret
ntfy:
authDefaultAccess: deny-all
database:
enabled: true
existingSecret: ntfy-database
existingSecretKey: database-url
persistence:
enabled: false
externalSecrets:
enabled: true
items:
- fullnameOverride: ntfy-database
spec:
secretStoreRef:
name: production-secrets
kind: ClusterSecretStore
target:
name: ntfy-database
data:
- secretKey: database-url
remoteRef:
key: ntfy/database
property: urlThe provider value must be a complete PostgreSQL connection URL. PostgreSQL replaces the message cache, access control, and web push SQLite stores. Keep persistence enabled if attachments, the abuse ban-feed, or other local files must survive pod replacement.
# 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.27.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, write-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.banFeed.enabled |
boolean | false |
Enable the weighted abuse ban-feed. |
ntfy.banFeed.file |
string | /var/cache/ntfy/ban.log |
Writable file where confirmed offenders are appended. |
ntfy.banFeed.window |
string | 10m |
Rolling window used to count weighted strikes per visitor prefix. |
ntfy.banFeed.threshold |
integer | 100 |
Weighted strikes per window before a prefix is emitted. |
ntfy.banFeed.weights |
array | ["42909:10"] |
Per-code weights in KEY:WEIGHT format; longest match wins. |
ntfy.database.enabled |
boolean | false |
Use PostgreSQL instead of the SQLite-backed stores. |
ntfy.database.existingSecret |
string | "" |
Secret containing the PostgreSQL connection URL. |
ntfy.database.existingSecretKey |
string | database-url |
Secret key exposed to ntfy as NTFY_DATABASE_URL. |
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 ntfy.database.enabled is true, the chart omits cache-file, auth-file, and web-push-file and loads the
database URL from the selected Secret. PostgreSQL stores messages, users, access control, and web push subscriptions.
This chart still deploys one ntfy replica. Local filesystem attachments are not shared, so PostgreSQL alone is not a complete highly available architecture.
External Secrets
| Parameter | Type | Default | Description |
|---|---|---|---|
externalSecrets.enabled |
boolean | false |
Render ExternalSecret resources. |
externalSecrets.refreshInterval |
string | 1h |
Default provider synchronization interval. |
externalSecrets.items |
array | [] |
ExternalSecret definitions with complete specs. |
Each item supports name, fullnameOverride, labels, annotations, and a complete ExternalSecret spec. Configure
either spec.secretStoreRef or a per-source sourceRef. The target Secret name must match
ntfy.database.existingSecret. See the
External Secrets Operator documentation for provider configuration.
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.
Enabling ntfy.banFeed only writes confirmed offenders to the configured file. Connect fail2ban or equivalent
enforcement separately, and rotate the file with copy-truncate semantics. The default file lives on the data PVC, so
it survives pod restarts when persistence is enabled.
Persistence
By default, ntfy stores its SQLite cache and authentication databases in /var/cache/ntfy. Persistence survives pod
restarts. With PostgreSQL, the PVC may be disabled only when attachments, the ban-feed, and custom file-backed options
do not require durable local storage.
| 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.