Skip to content

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.

Default access is open — secure before exposing publicly

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 via GET or 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 /metrics endpoint 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.com

After 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: Prefix

After 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.com

Configuration 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.
Append arbitrary configuration with extraConfig

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.

behindProxy should stay true on Kubernetes

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

Notifications arriving with wrong client IP

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.

Sending a test notification

After deploying, verify your setup with a quick curl command:

curl -d "Test notification from Kubernetes" https://ntfy.example.com/test-topic

Subscribe to https://ntfy.example.com/test-topic in the ntfy app to receive it.

More Information