ntfy turns a topic name into a phone notification, and the server is one Go binary
Send push notifications to your phone or desktop using PUT/POST
At a glance
- What is it?
- ntfy is an HTTP-based pub-sub notification service: a script PUTs to a topic and a phone app subscribes to it, with no account and no fee. The repository is the Go server, and its own README carries no install command at all, so a self-hoster has to start from the compose file, the Dockerfiles and the docs site.
- Who is it for?
- ntfy fits a homelab, a small team, or a script that has to page a human without provisioning a notification service first, and self-hosting is the better choice once the topic names carry operational meaning.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The topic name is the address, and HTTP is the transport
ntfy, pronounced notify, is described as a simple HTTP-based pub-sub notification service. Publishing means sending to a topic, subscribing means listening to one, and the topic name is the only address either side needs to agree on. That is the whole design: no account, no sign-up, no fee, and notifications reach a phone or a desktop from scripts on any computer. The delivery side exists as separate open source apps, an Android app on Google Play and F-Droid and an iOS app on the App Store, so the client is not locked to the hosted instance.
The Go module behind all of this is published as heckel.io/ntfy/v2, and pkg.go.dev hosts its documentation, which means the client code is something a Go program imports rather than something it shells out to. The module declares go 1.26.0, and a .go-version file at the repository root pins the same toolchain for contributors, with .goreleaser.yml handling packaged releases.
The README answers no install question, so the docs site carries that work
Worth knowing before you start: this README contains no install command, no config keys and no example of a request. What it offers is a documentation site with five named sections, Getting started, Android/iOS, API, Install / Self-hosting, and Building, and each is a separate page. Everything a self-hoster needs, from the config file format to the auth options, lives behind those links rather than in the repository front page.
The docs are themselves a build artifact in this tree. A mkdocs.yml at the root plus requirements.txt pinning mkdocs-material and mkdocs-minify-plugin means the site is MkDocs Material with minification, and the same requirement file carries a comment explaining that mkdocs is written in Python. So the documentation toolchain is Python on a project whose server is Go, and anyone regenerating the site needs both. A .gitpod.yml at the root and a Gitpod badge in the README point at the hosted alternative to building that environment locally.
The compose file publishes host port 80 and mounts config as a file
What the repository does ship for running the server is a compose file, and it is the fastest accurate picture of a self-hosted instance:
services:
ntfy:
image: binwiederhier/ntfy
container_name: ntfy
command:
- serve
environment:
- TZ=UTC # optional: Change to your desired timezone
user: UID:GID # optional: Set custom user/group or uid/gid
volumes:
- /var/cache/ntfy:/var/cache/ntfy
- /etc/ntfy:/etc/ntfy
ports:
- 80:80
restart: unless-stoppedThree details decide how much work an install is. Configuration arrives as a bind mount of /etc/ntfy rather than as environment variables, so the config file is the artifact you version and back up. Authentication and topics are not configured in this file at all, which tells you a fresh container starts with whatever the mounted file says and nothing more. And the process runs as UID:GID, written as a placeholder you have to replace with real values, since a container left on its default user cannot write the /var/cache/ntfy mount it is given.
The image is one binary copied into Alpine, listening on port 80
The Dockerfile is short enough to read in full, and it explains the shape of the deployment:
FROM alpine
RUN apk add --no-cache tzdata
COPY ntfy /usr/bin
EXPOSE 80/tcp
ENTRYPOINT ["ntfy"]There is no package manager, no shell tooling and no runtime installed beyond Alpine's base, and no source in the image. The ntfy binary is built elsewhere and copied in as a single file at /usr/bin, which is why the build stage is a separate concern: the tree carries Dockerfile-arm and Dockerfile-build alongside this one. The tzdata package is installed for one reason, so that the TZ environment variable the compose file sets has real zone information to resolve against.
The image labels state the licenses as Apache-2.0, GPL-2.0, matching the LICENSE and LICENSE.GPLv2 files at the root, and name the vendor and documentation site. Port 80 is both exposed and published, so a stock deployment listens on port 80 of the host, which is a decision to revisit before putting it behind anything.
The module graph names every backend the server talks to
go.mod is the most information-dense file in the tree, because each dependency is a place the server reaches outside itself. Two SQL drivers are compiled in: github.com/mattn/go-sqlite3 for a file-backed database and github.com/jackc/pgx/v5 for Postgres, so a single binary serves either and a migration is a config change rather than a rebuild. Delivery is a second cluster: firebase.google.com/go/v4 for Android push, github.com/SherClockHolmes/webpush-go for browser web push, github.com/emersion/go-smtp for the mail path, and github.com/stripe-go/v74 plus a payments/ directory for the paid tier.
The directory listing confirms the same split at a coarser level, with mail/, twilio/, webpush/, attachment/, s3/ and ban/ each covering one integration. The consequence for an operator is that this is a hub, not a pipe, and every one of those paths is a third-party account you supply credentials for. A twilio/ directory that exists in the source does not mean SMS is configured, and the compose file shows no environment variables for any of them.
One SMTP dependency is pinned to an old version on purpose
Near the bottom of go.mod sits a replace directive that contradicts the version requested above it:
module heckel.io/ntfy/v2
go 1.26.0replace github.com/emersion/go-smtp => github.com/emersion/go-smtp v0.17.0 // Pin version due to breaking changes, see #839The require block asks for go-smtp v0.25.0, and the replace rewrites it to v0.17.0 with the comment naming breaking changes and issue 839. That is a maintainer choosing an older release of a mail library over adapting to a newer one, and it is the kind of decision that belongs in a server project rather than in a library, since anyone importing heckel.io/ntfy/v2 inherits the pin. The second consequence is about the docs and the server: the mail path is maintained against an API two minor versions behind, so a change in that library will not arrive here until the pin is revisited.
Eleven example directories are the closest thing to an interface spec
The examples/ directory is where the protocol becomes concrete, and it is arranged as matching pairs. Publishing and subscribing are both shown in Go, PHP and Python, under publish-go, subscribe-go, publish-php, subscribe-php, publish-python and subscribe-python, so the same flow can be read in whichever language a reader already uses. Two web examples, web-example-eventsource and web-example-websocket, show the two subscription styles a browser can use.
The rest are whole use cases rather than fragments: ssh-login-alert, linux-desktop-notifications and a grafana-dashboard. That last one is the most informative for a team, because a dashboard example implies a metrics endpoint exists, and the tree carries a metrics/ directory along with github.com/prometheus/client_golang in the module. Nothing in the README explains the request format, so these directories are where a reader has to go, and a topic name that collides with a reserved one is a mistake the examples may or may not warn about.
The hosted instance and ntfy Pro sit on top of the same open source server
The business model is stated in the README directly. ntfy.sh is free to use with no sign-up, and the maintainer offers paid ntfy Pro plans for anyone who does not want to self-host, at a floor of $5 per month, with donations also accepted through GitHub Sponsors and Liberapay. The payments/ directory and the Stripe dependency are how that plan is charged for, which means the paid tier and the self-hosted server are the same codebase with a billing path attached rather than two products.
On movement, the last push to the main branch was on 2026-09-15, and the recent releases are v2.28.0 on 2026-08-27, v2.27.0 on 2026-08-04 and v2.26.3 on 2026-07-20, so tags land every few weeks with patch releases between them. The repository is not archived, and it carries SECURITY.md and CODE_OF_CONDUCT.md. For a self-hoster the practical split is that the $5 plan buys someone else running the container, while running it yourself costs the host and leaves the payments path switched off.
Editorial conclusion
ntfy fits a homelab, a small team, or a script that has to page a human without provisioning a notification service first, and self-hosting is the better choice once the topic names carry operational meaning. Before you commit, check two things: that the /etc/ntfy mount in the compose file holds the configuration you intend to use, and that the delivery path you depend on, Firebase for Android, web push, SMTP through mail/, or Twilio through twilio/, is one you can supply credentials for. Anyone who needs an audited SLA, a vendor relationship, or an Android build outside the project should look elsewhere, because ntfy.sh is a single maintainer's hosted instance and the paid floor on ntfy Pro is $5 a month.
Frequently asked questions
What is ntfy and what does it do?
ntfy, pronounced notify, is a simple HTTP-based pub-sub notification service. You can send notifications to your phone or desktop via scripts from any computer, without having to sign up or pay fees, and a free version is available at ntfy.sh with open source Android and iOS apps.
How much does ntfy cost to use?
The free version at ntfy.sh has no sign-up and no fees. The maintainer also offers paid ntfy Pro plans for anyone who would rather not self-host, for as low as $5 per month, and accepts donations through GitHub Sponsors and Liberapay.
Can ntfy be self-hosted?
Yes, the project is open source and the repository ships a Dockerfile, a Dockerfile-arm, a Dockerfile-build and a docker-compose.yml that runs the image binwiederhier/ntfy with the command serve, mounting /etc/ntfy for configuration and /var/cache/ntfy for its cache. Installation and self-hosting have their own section in the documentation at ntfy.sh/docs.
How do I get notifications from ntfy on iOS?
There is an open source iOS app published on the App Store, with beta builds distributed through TestFlight. The documentation has a dedicated Android/iOS section for subscribing on a phone, and release announcements go to the ntfy.sh/announcements topic on the hosted instance.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/binwiederhier-ntfy)