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
  • 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 /metrics endpoint 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.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 — 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: url

The 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.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.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.
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.

PostgreSQL replaces all SQLite stores

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.

PostgreSQL does not automatically enable HA

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.

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.

The ban-feed needs an external consumer and rotation

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

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