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 ntfy.authDefaultAccess: deny-all and configure users or OIDC 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
  • Secure runtime — non-root execution, read-only root filesystem, dropped capabilities, seccomp, and no API token
  • Production controls — resource bounds, bounded temporary storage, optional NetworkPolicy and PodDisruptionBudget
  • S3 backup — optional scheduled archive of the ntfy data volume

Architecture and Boundaries

The chart runs one ntfy Deployment behind a ClusterIP Service. ntfy listens on the configurable containerPorts.http port (2586 by default), while service.port remains the stable client-facing port. The ConfigMap is mounted read-only at /etc/ntfy; /tmp is a bounded emptyDir; and /var/cache/ntfy is backed by a PVC or a bounded ephemeral volume.

SQLite is the default store. PostgreSQL can come from the HelmForge subchart or an external service, but it does not make this single-replica chart highly available and does not provide shared attachment storage. Ingress and Gateway API are optional edge routes. The chart does not install an edge controller, cert-manager, Prometheus Operator, External Secrets Operator, an SMTP server, an OIDC provider, or an S3 service.

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.32.0. The chart remains single-replica because upstream clustering and local attachment sharing are outside its production boundary. Back up persistent state and any PostgreSQL database, then validate publishing, cache replay, authentication, email delivery, web push, and attachment workflows. Existing values 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.32.0 default. Review the 2.32.0 release:

helm upgrade ntfy helmforge/ntfy \
  --reset-then-reuse-values

ntfy 2.31.1 migrated the message database schema from version 16 to 17. After upgrading an instance from ntfy 2.30.0 or earlier to 2.32.0, rolling back to 2.31.0 or earlier requires manually resetting the recorded message schema version before starting the older server. For PostgreSQL, run UPDATE schema_version SET version = 16 WHERE store = 'message';. For the SQLite cache file, run UPDATE schemaVersion SET version = 16 WHERE id = 1;. Rolling back to 2.31.1 or later does not require this adjustment because those releases tolerate newer message schemas. Review the upstream 2.31.1 migration and rollback notes before downgrading.

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.
global object {} Reserved global values contract.

Image

Parameter Type Default Description
image.repository string docker.io/binwiederhier/ntfy ntfy container image.
image.tag string "v2.32.0" Image tag.
image.pullPolicy string IfNotPresent Image pull policy.
imagePullSecrets array [] Pull secrets for private registries.

ntfy Configuration

The listener is controlled only by containerPorts.http; attempts to override listen-http through extraConfig or NTFY_LISTEN_HTTP through extraEnv are rejected so the process, Service, and probes remain aligned.

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.
containerPorts.http integer 2586 Port on which the ntfy process listens.
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.
persistence.emptyDirSizeLimit string 1Gi Data-volume limit when persistence is disabled.

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 requests 50m/64Mi, limits 500m/256Mi CPU and memory requests and limits.
podSecurityContext object non-root UID/GID 1000, RuntimeDefault Pod-level security context.
securityContext object read-only, no escalation, drop ALL 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.
serviceAccount.automountServiceAccountToken boolean false Mount a Kubernetes API token into ntfy and backup pods.

Runtime Security and Availability

The defaults run as UID/GID 1000 with RuntimeDefault seccomp, a read-only root filesystem, no privilege escalation, and all capabilities dropped. CPU and memory requests and limits are set by default; tune them from observed usage.

Parameter Type Default Description
resources.requests.cpu string 50m Default CPU request.
resources.requests.memory string 64Mi Default memory request.
resources.limits.cpu string 500m Default CPU limit.
resources.limits.memory string 256Mi Default memory limit.
podSecurityContext.runAsNonRoot boolean true Require non-root execution.
podSecurityContext.runAsUser integer 1000 Pod user ID.
podSecurityContext.runAsGroup integer 1000 Pod group ID.
podSecurityContext.fsGroup integer 1000 Writable-volume group ID.
podSecurityContext.fsGroupChangePolicy string OnRootMismatch Avoid unnecessary recursive ownership changes.
podSecurityContext.seccompProfile.type string RuntimeDefault Container syscall profile.
securityContext.allowPrivilegeEscalation boolean false Deny privilege escalation.
securityContext.readOnlyRootFilesystem boolean true Keep the image filesystem read-only.
securityContext.capabilities.drop array ["ALL"] Drop Linux capabilities.
runtimeStorage.tmpSizeLimit string 128Mi Bound writable /tmp storage.
podDisruptionBudget.enabled boolean false Render a disruption budget for the singleton pod.
podDisruptionBudget.minAvailable integer 1 Keep one pod available; on a singleton, voluntary eviction and node drain wait until this value changes or the PDB is removed.
podDisruptionBudget.maxUnavailable integer null Alternative maximum unavailable pod count.

Network Policy

Ingress isolation can be enabled independently. When egress isolation is enabled, list every external dependency. External PostgreSQL requires an explicit database CIDR; SMTP, OIDC, web push, and S3 need suitable extraEgress rules.

Parameter Type Default Description
networkPolicy.enabled boolean false Render a NetworkPolicy.
networkPolicy.ingressFrom array [] Peers allowed to reach the ntfy and optional metrics ports.
networkPolicy.egress.enabled boolean false Apply egress isolation.
networkPolicy.egress.allowDNS boolean true Permit DNS when egress isolation is active.
networkPolicy.egress.dnsPeers array kube-system Peers allowed to serve DNS; override for custom DNS.
networkPolicy.egress.externalDatabaseCIDR string "" CIDR allowed for external PostgreSQL.
networkPolicy.egress.extraEgress array [] Complete additional egress rules for external integrations.

Backup

The optional CronJob archives /var/cache/ntfy and uploads it to S3-compatible storage. It requires persistent storage and does not dump PostgreSQL. Use an existing Secret containing access-key and secret-key in production.

Parameter Type Default Description
backup.enabled boolean false Enable scheduled data-volume backup.
backup.schedule string 0 3 * * * Cron schedule in UTC.
backup.scratchSizeLimit string 3Gi Bound temporary archive storage with headroom.
backup.tmpSizeLimit string 64Mi Bound backup container /tmp.
backup.s3.endpoint string "" S3-compatible endpoint.
backup.s3.bucket string "" Destination bucket.
backup.s3.prefix string ntfy Object-key prefix.
backup.s3.existingSecret string "" Existing S3 credential Secret.

OpenID Connect and SMTP

OIDC client secrets and SMTP passwords are injected from Secrets through environment variables and never written to the ConfigMap. Restart the Deployment after rotating an existing Secret.

Parameter Type Default Description
oidc.enabled boolean false Enable OpenID Connect authentication.
oidc.issuerUrl string "" Identity-provider issuer URL.
oidc.clientId string "" Registered client ID.
oidc.existingSecret string "" Secret containing the client secret.
oidc.existingSecretKey string client-secret Key containing the client secret.
smtp.enabled boolean false Enable outbound email notifications.
smtp.senderAddr string "" SMTP server in host:port form.
smtp.senderFrom string "" Sender email address.
smtp.existingSecret string "" Secret containing the SMTP password.
smtp.existingSecretPasswordKey string smtp-password Key containing the SMTP password.

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