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

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

Configuration Reference

Core

ParameterTypeDefaultDescription
nameOverridestring""Override the chart name.
fullnameOverridestring""Override the full release name.
commonLabelsobject{}Extra labels added to all resources.

Image

ParameterTypeDefaultDescription
image.repositorystringdocker.io/binwiederhier/ntfyntfy container image.
image.tagstring"v2.21.0"Image tag.
image.pullPolicystringIfNotPresentImage pull policy.
imagePullSecretsarray[]Pull secrets for private registries.

ntfy Configuration

ParameterTypeDefaultDescription
ntfy.baseUrlstring""Public base URL of the instance (e.g. https://ntfy.example.com).
ntfy.authDefaultAccessstringread-writeDefault access for unauthenticated users: read-write, read-only, deny-all.
ntfy.behindProxybooleantrueTrust X-Forwarded-For headers for correct client IP and rate limiting.
ntfy.enableMetricsbooleanfalseEnable Prometheus metrics at /metrics.
ntfy.metricsListenHttpstring""Separate listen address for the metrics endpoint (e.g. :9090).
ntfy.attachmentTotalSizeLimitstring""Total attachment storage limit per visitor (e.g. 100M).
ntfy.attachmentFileSizeLimitstring""Maximum size per attachment file (e.g. 15M).
ntfy.attachmentExpiryDurationstring""How long attachments are retained (e.g. 3h).
ntfy.extraConfigstring""Raw server.yml configuration appended to the generated ConfigMap.
ntfy.extraEnvarray[]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.

ParameterTypeDefaultDescription
persistence.enabledbooleantrueEnable a PVC for /var/cache/ntfy.
persistence.sizestring2GiPVC size. Increase if using attachments.
persistence.storageClassstring""StorageClass for the PVC.
persistence.accessModesarray["ReadWriteOnce"]PVC access modes.
persistence.existingClaimstring""Use an existing PVC instead of creating one.

Service

ParameterTypeDefaultDescription
service.typestringClusterIPKubernetes service type.
service.portinteger80Service port exposed to the cluster.
service.annotationsobject{}Annotations for the Service.

Ingress

ParameterTypeDefaultDescription
ingress.enabledbooleanfalseEnable an Ingress resource.
ingress.ingressClassNamestringtraefikIngress class name.
ingress.annotationsobject{}Annotations for the Ingress (e.g. cert-manager).
ingress.hostsarray[]Ingress host and path rules.
ingress.tlsarray[]TLS configuration (secret name and hosts).

Probes

Probes use the /v1/health endpoint.

ParameterTypeDefaultDescription
probes.startup.enabledbooleantrueEnable startup probe.
probes.startup.initialDelaySecondsinteger5Startup probe initial delay.
probes.startup.periodSecondsinteger5Startup probe period.
probes.startup.timeoutSecondsinteger3Startup probe timeout.
probes.startup.failureThresholdinteger30Startup probe failure threshold.
probes.liveness.enabledbooleantrueEnable liveness probe.
probes.liveness.initialDelaySecondsinteger0Liveness probe initial delay.
probes.liveness.periodSecondsinteger15Liveness probe period.
probes.liveness.timeoutSecondsinteger5Liveness probe timeout.
probes.liveness.failureThresholdinteger3Liveness probe failure threshold.
probes.readiness.enabledbooleantrueEnable readiness probe.
probes.readiness.initialDelaySecondsinteger0Readiness probe initial delay.
probes.readiness.periodSecondsinteger10Readiness probe period.
probes.readiness.timeoutSecondsinteger5Readiness probe timeout.
probes.readiness.failureThresholdinteger3Readiness probe failure threshold.

Metrics

ParameterTypeDefaultDescription
metrics.serviceMonitor.enabledbooleanfalseCreate a ServiceMonitor for Prometheus Operator.
metrics.serviceMonitor.intervalstring30sPrometheus scrape interval.
metrics.serviceMonitor.labelsobject{}Extra labels applied to the ServiceMonitor.

Resources and Security

ParameterTypeDefaultDescription
resourcesobject{}CPU and memory requests and limits.
podSecurityContextobject{}Pod-level security context.
securityContextobject{}Container-level security context.

Service Account

ParameterTypeDefaultDescription
serviceAccount.createbooleanfalseCreate a dedicated ServiceAccount.
serviceAccount.namestring""Override the ServiceAccount name.
serviceAccount.annotationsobject{}Annotations for the ServiceAccount.

Scheduling

ParameterTypeDefaultDescription
nodeSelectorobject{}Node selector for scheduling.
tolerationsarray[]Tolerations for scheduling.
affinityobject{}Affinity rules.
topologySpreadConstraintsarray[]Topology spread constraints.
priorityClassNamestring""PriorityClass for the pod.
terminationGracePeriodSecondsinteger30Termination grace period.
podLabelsobject{}Extra labels for the pod.
podAnnotationsobject{}Extra annotations for the pod.

Extra

ParameterTypeDefaultDescription
extraVolumesarray[]Extra volumes to attach to the pod.
extraVolumeMountsarray[]Extra volume mounts for the container.
extraManifestsarray[]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