From 9f3883eaf057e6347bfea288b9dc22bc5a7186d1 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 17 Jan 2026 14:00:08 -0500 Subject: [PATCH 1/6] Build server on Windows --- .goreleaser.yml | 14 ++++++++------ Makefile | 16 +++++++++++++++- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index f0cf08f6..3c4e9c76 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -48,13 +48,15 @@ builds: - id: ntfy_windows_amd64 binary: ntfy env: - - CGO_ENABLED=0 # explicitly disable, since we don't need go-sqlite3 - tags: [ noserver ] # don't include server files + - CGO_ENABLED=1 # required for go-sqlite3 + - CC=x86_64-w64-mingw32-gcc # apt install gcc-mingw-w64-x86-64 + tags: [ sqlite_omit_load_extension,osusergo,netgo ] ldflags: - - "-X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}" + - "-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}" goos: [ windows ] - goarch: [ amd64 ] - - id: ntfy_darwin_all + goarch: [amd64 ] + - + id: ntfy_darwin_all binary: ntfy env: - CGO_ENABLED=0 # explicitly disable, since we don't need go-sqlite3 @@ -201,4 +203,4 @@ docker_manifests: - *amd64_image - *arm64v8_image - *armv7_image - - *armv6_image \ No newline at end of file + - *armv6_image diff --git a/Makefile b/Makefile index df131c7a..beac6b8f 100644 --- a/Makefile +++ b/Makefile @@ -31,6 +31,7 @@ help: @echo "Build server & client (without GoReleaser):" @echo " make cli-linux-server - Build client & server (no GoReleaser, current arch, Linux)" @echo " make cli-darwin-server - Build client & server (no GoReleaser, current arch, macOS)" + @echo " make cli-windows-server - Build client & server (no GoReleaser, amd64 only, Windows)" @echo " make cli-client - Build client only (no GoReleaser, current arch, Linux/macOS/Windows)" @echo @echo "Build dev Docker:" @@ -201,6 +202,16 @@ cli-darwin-server: cli-deps-static-sites -ldflags \ "-linkmode=external -s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(shell date +%s)" +cli-windows-server: cli-deps-static-sites + # This is a target to build the CLI (including the server) for Windows. + # Use this for Windows development, if you really don't want to install GoReleaser ... + mkdir -p dist/ntfy_windows_server server/docs + CC=x86_64-w64-mingw32-gcc GOOS=windows GOARCH=amd64 CGO_ENABLED=1 go build \ + -o dist/ntfy_windows_server/ntfy.exe \ + -tags sqlite_omit_load_extension,osusergo,netgo \ + -ldflags \ + "-s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(shell date +%s)" + cli-client: cli-deps-static-sites # This is a target to build the CLI (excluding the server) manually. This should work on Linux/macOS/Windows. # Use this for development, if you really don't want to install GoReleaser ... @@ -213,7 +224,7 @@ cli-client: cli-deps-static-sites cli-deps: cli-deps-static-sites cli-deps-all cli-deps-gcc -cli-deps-gcc: cli-deps-gcc-armv6-armv7 cli-deps-gcc-arm64 +cli-deps-gcc: cli-deps-gcc-armv6-armv7 cli-deps-gcc-arm64 cli-deps-gcc-windows cli-deps-static-sites: mkdir -p server/docs server/site @@ -228,6 +239,9 @@ cli-deps-gcc-armv6-armv7: cli-deps-gcc-arm64: which aarch64-linux-gnu-gcc || { echo "ERROR: ARM64 cross compiler not installed. On Ubuntu, run: apt install gcc-aarch64-linux-gnu"; exit 1; } +cli-deps-gcc-windows: + which x86_64-w64-mingw32-gcc || { echo "ERROR: Windows cross compiler not installed. On Ubuntu, run: apt install gcc-mingw-w64-x86-64"; exit 1; } + cli-deps-update: go get -u go install honnef.co/go/tools/cmd/staticcheck@latest From 6d5cc6aeac8828413d47c54f46ff99b90b2fed47 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 17 Jan 2026 14:43:43 -0500 Subject: [PATCH 2/6] Windows server support --- cmd/serve.go | 51 +++++-------------- cmd/serve_unix.go | 57 +++++++++++++++++++++ cmd/serve_windows.go | 104 +++++++++++++++++++++++++++++++++++++++ docs/config.md | 78 +++++++++++++++++++++++++++++ docs/install.md | 25 ++++++++-- go.mod | 2 +- server/config.go | 8 ++- server/config_unix.go | 8 +++ server/config_windows.go | 17 +++++++ 9 files changed, 304 insertions(+), 46 deletions(-) create mode 100644 cmd/serve_unix.go create mode 100644 cmd/serve_windows.go create mode 100644 server/config_unix.go create mode 100644 server/config_windows.go diff --git a/cmd/serve.go b/cmd/serve.go index 4d2803d5..dfbdc0a9 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -10,10 +10,8 @@ import ( "net" "net/netip" "net/url" - "os" - "os/signal" + "runtime" "strings" - "syscall" "text/template" "time" @@ -350,6 +348,8 @@ func execServe(c *cli.Context) error { return errors.New("visitor-prefix-bits-ipv4 must be between 1 and 32") } else if visitorPrefixBitsIPv6 < 1 || visitorPrefixBitsIPv6 > 128 { return errors.New("visitor-prefix-bits-ipv6 must be between 1 and 128") + } else if runtime.GOOS == "windows" && listenUnix != "" { + return errors.New("listen-unix is not supported on Windows") } // Backwards compatibility @@ -503,6 +503,14 @@ func execServe(c *cli.Context) error { conf.WebPushExpiryWarningDuration = webPushExpiryWarningDuration conf.Version = c.App.Version + // Check if we should run as a Windows service + if ranAsService, err := maybeRunAsService(conf); err != nil { + log.Fatal("%s", err.Error()) + } else if ranAsService { + log.Info("Exiting.") + return nil + } + // Set up hot-reloading of config go sigHandlerConfigReload(config) @@ -517,22 +525,6 @@ func execServe(c *cli.Context) error { return nil } -func sigHandlerConfigReload(config string) { - sigs := make(chan os.Signal, 1) - signal.Notify(sigs, syscall.SIGHUP) - for range sigs { - log.Info("Partially hot reloading configuration ...") - inputSource, err := newYamlSourceFromFile(config, flagsServe) - if err != nil { - log.Warn("Hot reload failed: %s", err.Error()) - continue - } - if err := reloadLogLevel(inputSource); err != nil { - log.Warn("Reloading log level failed: %s", err.Error()) - } - } -} - func parseIPHostPrefix(host string) (prefixes []netip.Prefix, err error) { // Try parsing as prefix, e.g. 10.0.1.0/24 or 2001:db8::/32 prefix, err := netip.ParsePrefix(host) @@ -664,24 +656,3 @@ func parseTokens(users []*user.User, tokensRaw []string) (map[string][]*user.Tok return tokens, nil } -func reloadLogLevel(inputSource altsrc.InputSourceContext) error { - newLevelStr, err := inputSource.String("log-level") - if err != nil { - return fmt.Errorf("cannot load log level: %s", err.Error()) - } - overrides, err := inputSource.StringSlice("log-level-overrides") - if err != nil { - return fmt.Errorf("cannot load log level overrides (1): %s", err.Error()) - } - log.ResetLevelOverrides() - if err := applyLogLevelOverrides(overrides); err != nil { - return fmt.Errorf("cannot load log level overrides (2): %s", err.Error()) - } - log.SetLevel(log.ToLevel(newLevelStr)) - if len(overrides) > 0 { - log.Info("Log level is %v, %d override(s) in place", strings.ToUpper(newLevelStr), len(overrides)) - } else { - log.Info("Log level is %v", strings.ToUpper(newLevelStr)) - } - return nil -} diff --git a/cmd/serve_unix.go b/cmd/serve_unix.go new file mode 100644 index 00000000..2c612338 --- /dev/null +++ b/cmd/serve_unix.go @@ -0,0 +1,57 @@ +//go:build linux || dragonfly || freebsd || netbsd || openbsd + +package cmd + +import ( + "os" + "os/signal" + "syscall" + + "github.com/urfave/cli/v2/altsrc" + "heckel.io/ntfy/v2/log" + "heckel.io/ntfy/v2/server" +) + +func sigHandlerConfigReload(config string) { + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGHUP) + for range sigs { + log.Info("Partially hot reloading configuration ...") + inputSource, err := newYamlSourceFromFile(config, flagsServe) + if err != nil { + log.Warn("Hot reload failed: %s", err.Error()) + continue + } + if err := reloadLogLevel(inputSource); err != nil { + log.Warn("Reloading log level failed: %s", err.Error()) + } + } +} + +func reloadLogLevel(inputSource altsrc.InputSourceContext) error { + newLevelStr, err := inputSource.String("log-level") + if err != nil { + return err + } + overrides, err := inputSource.StringSlice("log-level-overrides") + if err != nil { + return err + } + log.ResetLevelOverrides() + if err := applyLogLevelOverrides(overrides); err != nil { + return err + } + log.SetLevel(log.ToLevel(newLevelStr)) + if len(overrides) > 0 { + log.Info("Log level is %v, %d override(s) in place", newLevelStr, len(overrides)) + } else { + log.Info("Log level is %v", newLevelStr) + } + return nil +} + +// maybeRunAsService is a no-op on Unix systems. +// Windows service mode is not available on Unix. +func maybeRunAsService(conf *server.Config) (bool, error) { + return false, nil +} diff --git a/cmd/serve_windows.go b/cmd/serve_windows.go new file mode 100644 index 00000000..c5e93c89 --- /dev/null +++ b/cmd/serve_windows.go @@ -0,0 +1,104 @@ +//go:build windows && !noserver + +package cmd + +import ( + "fmt" + "sync" + + "golang.org/x/sys/windows/svc" + "heckel.io/ntfy/v2/log" + "heckel.io/ntfy/v2/server" +) + +const serviceName = "ntfy" + +// sigHandlerConfigReload is a no-op on Windows since SIGHUP is not available. +// Windows users can restart the service to reload configuration. +func sigHandlerConfigReload(config string) { + log.Debug("Config hot-reload via SIGHUP is not supported on Windows") + // On Windows, we simply don't set up any signal handler for config reload. + // Users must restart the service/process to reload configuration. +} + +// runAsWindowsService runs the ntfy server as a Windows service +func runAsWindowsService(conf *server.Config) error { + return svc.Run(serviceName, &ntfyService{conf: conf}) +} + +// ntfyService implements the svc.Handler interface +type ntfyService struct { + conf *server.Config + server *server.Server + mu sync.Mutex +} + +// Execute is the main entry point for the Windows service +func (s *ntfyService) Execute(args []string, requests <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) { + const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown + + status <- svc.Status{State: svc.StartPending} + + // Create and start the server + var err error + s.mu.Lock() + s.server, err = server.New(s.conf) + s.mu.Unlock() + if err != nil { + log.Error("Failed to create server: %s", err.Error()) + return true, 1 + } + + // Start server in a goroutine + serverErrChan := make(chan error, 1) + go func() { + serverErrChan <- s.server.Run() + }() + + status <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} + log.Info("Windows service started") + + for { + select { + case err := <-serverErrChan: + if err != nil { + log.Error("Server error: %s", err.Error()) + return true, 1 + } + return false, 0 + case req := <-requests: + switch req.Cmd { + case svc.Interrogate: + status <- req.CurrentStatus + case svc.Stop, svc.Shutdown: + log.Info("Windows service stopping...") + status <- svc.Status{State: svc.StopPending} + s.mu.Lock() + if s.server != nil { + s.server.Stop() + } + s.mu.Unlock() + return false, 0 + default: + log.Warn("Unexpected service control request: %d", req.Cmd) + } + } + } +} + +// maybeRunAsService checks if the process is running as a Windows service, +// and if so, runs the server as a service. Returns true if it ran as a service. +func maybeRunAsService(conf *server.Config) (bool, error) { + isService, err := svc.IsWindowsService() + if err != nil { + return false, fmt.Errorf("failed to detect Windows service mode: %w", err) + } + if !isService { + return false, nil + } + log.Info("Running as Windows service") + if err := runAsWindowsService(conf); err != nil { + return true, fmt.Errorf("failed to run as Windows service: %w", err) + } + return true, nil +} diff --git a/docs/config.md b/docs/config.md index 8a125146..584f4064 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1060,6 +1060,84 @@ or the root domain: } ``` +## Windows service +ntfy can run as a Windows service, allowing it to start automatically on boot and run in the background. +ntfy automatically detects when it is running as a Windows service and adjusts its behavior accordingly. + +### Installing the service +To install ntfy as a Windows service, open an **Administrator** command prompt or PowerShell and run: + +```cmd +sc create ntfy binPath= "C:\path\to\ntfy.exe serve" start= auto +``` + +!!! note + Make sure to replace `C:\path\to\ntfy.exe` with the actual path to your ntfy executable. + The spaces after `binPath=` and `start=` are required. + +You can also specify a config file: + +```cmd +sc create ntfy binPath= "C:\path\to\ntfy.exe serve --config C:\ProgramData\ntfy\server.yml" start= auto +``` + +### Starting and stopping +To start the service: + +```cmd +sc start ntfy +``` + +To stop the service: + +```cmd +sc stop ntfy +``` + +To check the service status: + +```cmd +sc query ntfy +``` + +### Configuring the service +The default configuration file location on Windows is `%ProgramData%\ntfy\server.yml` (typically `C:\ProgramData\ntfy\server.yml`). +Create this directory and file manually if needed. + +Example minimal config: + +```yaml +base-url: "https://ntfy.example.com" +listen-http: ":80" +cache-file: "C:\\ProgramData\\ntfy\\cache.db" +auth-file: "C:\\ProgramData\\ntfy\\auth.db" +``` + +!!! warning + Use double backslashes (`\\`) for paths in YAML files on Windows, or use forward slashes (`/`). + +### Viewing logs +By default, ntfy logs to stderr. When running as a Windows service, you can configure logging to a file: + +```yaml +log-file: "C:\\ProgramData\\ntfy\\ntfy.log" +log-level: info +``` + +### Removing the service +To remove the ntfy service: + +```cmd +sc stop ntfy +sc delete ntfy +``` + +### Limitations on Windows +When running on Windows, the following features are not available: + +- **Unix socket listening**: The `listen-unix` option is not supported on Windows +- **Config hot-reload**: The SIGHUP signal for hot-reloading configuration is not available on Windows; restart the service to apply config changes + ## Firebase (FCM) !!! info Using Firebase is **optional** and only works if you modify and [build your own Android .apk](develop.md#android-app). diff --git a/docs/install.md b/docs/install.md index dc50e222..a437499e 100644 --- a/docs/install.md +++ b/docs/install.md @@ -228,20 +228,39 @@ brew install ntfy ``` ## Windows -The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well. +The ntfy server and CLI are fully supported on Windows. You can run the ntfy server directly or as a Windows service. + To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_windows_amd64.zip), extract it and place the `ntfy.exe` binary somewhere in your `%Path%`. -The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file). - Also available in [Scoop's](https://scoop.sh) Main repository: `scoop install ntfy` +### Running the server +To run the ntfy server directly: +``` +ntfy serve +``` + +The default configuration file location on Windows is `%ProgramData%\ntfy\server.yml` (e.g., `C:\ProgramData\ntfy\server.yml`). +You may need to create the directory and config file manually. + +For information on running ntfy as a Windows service, see the [Windows service](config.md#windows-service) section in the configuration documentation. + +### Client configuration +The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file). + !!! info There is currently no installer for Windows, and the binary is not signed. If this is desired, please create a [GitHub issue](https://github.com/binwiederhier/ntfy/issues) to let me know. +!!! note + Some features are not available on Windows: + + - Unix socket listening (`listen-unix`) is not supported + - Config hot-reload via SIGHUP is not available; restart the service to apply config changes + ## Docker The [ntfy image](https://hub.docker.com/r/binwiederhier/ntfy) is available for amd64, armv6, armv7 and arm64. It should be pretty straight forward to use. diff --git a/go.mod b/go.mod index ad61782e..1f7f6b4c 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,7 @@ require ( github.com/microcosm-cc/bluemonday v1.0.27 github.com/prometheus/client_golang v1.23.2 github.com/stripe/stripe-go/v74 v74.30.0 + golang.org/x/sys v0.40.0 golang.org/x/text v0.33.0 ) @@ -93,7 +94,6 @@ require ( go.opentelemetry.io/otel/trace v1.39.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/net v0.49.0 // indirect - golang.org/x/sys v0.40.0 // indirect google.golang.org/appengine/v2 v2.0.6 // indirect google.golang.org/genproto v0.0.0-20260114163908-3f89685c29c3 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260114163908-3f89685c29c3 // indirect diff --git a/server/config.go b/server/config.go index c4c76bd1..e52556f2 100644 --- a/server/config.go +++ b/server/config.go @@ -12,8 +12,6 @@ import ( // Defines default config settings (excluding limits, see below) const ( DefaultListenHTTP = ":80" - DefaultConfigFile = "/etc/ntfy/server.yml" - DefaultTemplateDir = "/etc/ntfy/templates" DefaultCacheDuration = 12 * time.Hour DefaultCacheBatchTimeout = time.Duration(0) DefaultKeepaliveInterval = 45 * time.Second // Not too frequently to save battery (Android read timeout used to be 77s!) @@ -27,6 +25,12 @@ const ( DefaultStripePriceCacheDuration = 3 * time.Hour // Time to keep Stripe prices cached in memory before a refresh is needed ) +// Platform-specific default paths (set in config_unix.go or config_windows.go) +var ( + DefaultConfigFile string + DefaultTemplateDir string +) + // Defines default Web Push settings const ( DefaultWebPushExpiryWarningDuration = 55 * 24 * time.Hour diff --git a/server/config_unix.go b/server/config_unix.go new file mode 100644 index 00000000..5b9812b2 --- /dev/null +++ b/server/config_unix.go @@ -0,0 +1,8 @@ +//go:build !windows + +package server + +func init() { + DefaultConfigFile = "/etc/ntfy/server.yml" + DefaultTemplateDir = "/etc/ntfy/templates" +} diff --git a/server/config_windows.go b/server/config_windows.go new file mode 100644 index 00000000..fc883c38 --- /dev/null +++ b/server/config_windows.go @@ -0,0 +1,17 @@ +//go:build windows + +package server + +import ( + "os" + "path/filepath" +) + +func init() { + programData := os.Getenv("ProgramData") + if programData == "" { + programData = `C:\ProgramData` + } + DefaultConfigFile = filepath.Join(programData, "ntfy", "server.yml") + DefaultTemplateDir = filepath.Join(programData, "ntfy", "templates") +} From dfd12528f3bd74f166be5d619fe99bfe29e12263 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 17 Jan 2026 14:48:32 -0500 Subject: [PATCH 3/6] Manual nits --- cmd/serve_unix.go | 2 -- cmd/serve_windows.go | 14 +++++--------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/cmd/serve_unix.go b/cmd/serve_unix.go index 2c612338..f12bb85b 100644 --- a/cmd/serve_unix.go +++ b/cmd/serve_unix.go @@ -50,8 +50,6 @@ func reloadLogLevel(inputSource altsrc.InputSourceContext) error { return nil } -// maybeRunAsService is a no-op on Unix systems. -// Windows service mode is not available on Unix. func maybeRunAsService(conf *server.Config) (bool, error) { return false, nil } diff --git a/cmd/serve_windows.go b/cmd/serve_windows.go index c5e93c89..e917a079 100644 --- a/cmd/serve_windows.go +++ b/cmd/serve_windows.go @@ -17,26 +17,23 @@ const serviceName = "ntfy" // Windows users can restart the service to reload configuration. func sigHandlerConfigReload(config string) { log.Debug("Config hot-reload via SIGHUP is not supported on Windows") - // On Windows, we simply don't set up any signal handler for config reload. - // Users must restart the service/process to reload configuration. } // runAsWindowsService runs the ntfy server as a Windows service func runAsWindowsService(conf *server.Config) error { - return svc.Run(serviceName, &ntfyService{conf: conf}) + return svc.Run(serviceName, &windowsService{conf: conf}) } -// ntfyService implements the svc.Handler interface -type ntfyService struct { +// windowsService implements the svc.Handler interface +type windowsService struct { conf *server.Config server *server.Server mu sync.Mutex } // Execute is the main entry point for the Windows service -func (s *ntfyService) Execute(args []string, requests <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) { +func (s *windowsService) Execute(args []string, requests <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) { const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown - status <- svc.Status{State: svc.StartPending} // Create and start the server @@ -92,8 +89,7 @@ func maybeRunAsService(conf *server.Config) (bool, error) { isService, err := svc.IsWindowsService() if err != nil { return false, fmt.Errorf("failed to detect Windows service mode: %w", err) - } - if !isService { + } else if !isService { return false, nil } log.Info("Running as Windows service") From 353fedb93f5c6d2eb68c40d5c5ff79f14d70a6a6 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 17 Jan 2026 14:59:43 -0500 Subject: [PATCH 4/6] Docs, lint --- cmd/serve.go | 1 - docs/releases.md | 3 ++- server/config.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index dfbdc0a9..5acf048b 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -655,4 +655,3 @@ func parseTokens(users []*user.User, tokensRaw []string) (map[string][]*user.Tok } return tokens, nil } - diff --git a/docs/releases.md b/docs/releases.md index 4c950b3b..e3e9ab50 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1605,7 +1605,8 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Support for [updating and deleting notifications](publish.md#updating-deleting-notifications) ([#303](https://github.com/binwiederhier/ntfy/issues/303), [#1536](https://github.com/binwiederhier/ntfy/pull/1536), [ntfy-android#151](https://github.com/binwiederhier/ntfy-android/pull/151), thanks to [@wunter8](https://github.com/wunter8) for the initial implementation) -* Support for a [custom Twilio call format](config.md#phone-calls) ([#1289](https://github.com/binwiederhier/ntfy/pull/1289), thanks to [@mmichaa](https://github.com/mmichaa) for the initial implementation) +* Configure [custom Twilio call format](config.md#phone-calls) for phone calls ([#1289](https://github.com/binwiederhier/ntfy/pull/1289), thanks to [@mmichaa](https://github.com/mmichaa) for the initial implementation) +* `ntfy serve` now works on Windows, including support for running it as a Windows service ([#1552](https://github.com/binwiederhier/ntfy/pull/1552), originally [#1328](https://github.com/binwiederhier/ntfy/pull/1328), thanks to [@wtf911](https://github.com/wtf911)) ### ntfy Android app v1.22.x (UNRELEASED) diff --git a/server/config.go b/server/config.go index e52556f2..804c0980 100644 --- a/server/config.go +++ b/server/config.go @@ -27,7 +27,7 @@ const ( // Platform-specific default paths (set in config_unix.go or config_windows.go) var ( - DefaultConfigFile string + DefaultConfigFile string DefaultTemplateDir string ) From 220372d65a49877a040c515887769b3b87d41a81 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 17 Jan 2026 17:51:33 -0500 Subject: [PATCH 5/6] Move client config file logic, docs --- client/config.go | 3 ++ client/config_darwin.go | 18 ++++++++++ client/config_unix.go | 18 ++++++++++ client/config_windows.go | 14 ++++++++ cmd/subscribe.go | 54 +++++----------------------- cmd/subscribe_darwin.go | 6 ++-- cmd/subscribe_unix.go | 4 --- cmd/subscribe_windows.go | 6 ++-- docs/config.md | 78 ---------------------------------------- docs/install.md | 35 +++++++----------- 10 files changed, 79 insertions(+), 157 deletions(-) create mode 100644 client/config_darwin.go create mode 100644 client/config_unix.go create mode 100644 client/config_windows.go diff --git a/client/config.go b/client/config.go index 870c835b..444460d6 100644 --- a/client/config.go +++ b/client/config.go @@ -11,6 +11,9 @@ const ( DefaultBaseURL = "https://ntfy.sh" ) +// DefaultConfigFile is the default path to the client config file (set in config_*.go) +var DefaultConfigFile string + // Config is the config struct for a Client type Config struct { DefaultHost string `yaml:"default-host"` diff --git a/client/config_darwin.go b/client/config_darwin.go new file mode 100644 index 00000000..c2488849 --- /dev/null +++ b/client/config_darwin.go @@ -0,0 +1,18 @@ +//go:build darwin + +package client + +import ( + "os" + "os/user" + "path/filepath" +) + +func init() { + u, err := user.Current() + if err == nil && u.Uid == "0" { + DefaultConfigFile = "/etc/ntfy/client.yml" + } else if configDir, err := os.UserConfigDir(); err == nil { + DefaultConfigFile = filepath.Join(configDir, "ntfy", "client.yml") + } +} diff --git a/client/config_unix.go b/client/config_unix.go new file mode 100644 index 00000000..273340e1 --- /dev/null +++ b/client/config_unix.go @@ -0,0 +1,18 @@ +//go:build linux || dragonfly || freebsd || netbsd || openbsd + +package client + +import ( + "os" + "os/user" + "path/filepath" +) + +func init() { + u, err := user.Current() + if err == nil && u.Uid == "0" { + DefaultConfigFile = "/etc/ntfy/client.yml" + } else if configDir, err := os.UserConfigDir(); err == nil { + DefaultConfigFile = filepath.Join(configDir, "ntfy", "client.yml") + } +} diff --git a/client/config_windows.go b/client/config_windows.go new file mode 100644 index 00000000..2ee55328 --- /dev/null +++ b/client/config_windows.go @@ -0,0 +1,14 @@ +//go:build windows + +package client + +import ( + "os" + "path/filepath" +) + +func init() { + if configDir, err := os.UserConfigDir(); err == nil { + DefaultConfigFile = filepath.Join(configDir, "ntfy", "client.yml") + } +} diff --git a/cmd/subscribe.go b/cmd/subscribe.go index 5ebf9627..84450927 100644 --- a/cmd/subscribe.go +++ b/cmd/subscribe.go @@ -3,28 +3,21 @@ package cmd import ( "errors" "fmt" + "os" + "os/exec" + "sort" + "strings" + "github.com/urfave/cli/v2" "heckel.io/ntfy/v2/client" "heckel.io/ntfy/v2/log" "heckel.io/ntfy/v2/util" - "os" - "os/exec" - "os/user" - "path/filepath" - "sort" - "strings" ) func init() { commands = append(commands, cmdSubscribe) } -const ( - clientRootConfigFileUnixAbsolute = "/etc/ntfy/client.yml" - clientUserConfigFileUnixRelative = "ntfy/client.yml" - clientUserConfigFileWindowsRelative = "ntfy\\client.yml" -) - var flagsSubscribe = append( append([]cli.Flag{}, flagsDefault...), &cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "client config file"}, @@ -310,45 +303,16 @@ func loadConfig(c *cli.Context) (*client.Config, error) { if filename != "" { return client.LoadConfig(filename) } - configFile, err := defaultClientConfigFile() - if err != nil { - log.Warn("Could not determine default client config file: %s", err.Error()) - } else { - if s, _ := os.Stat(configFile); s != nil { - return client.LoadConfig(configFile) + if client.DefaultConfigFile != "" { + if s, _ := os.Stat(client.DefaultConfigFile); s != nil { + return client.LoadConfig(client.DefaultConfigFile) } - log.Debug("Config file %s not found", configFile) + log.Debug("Config file %s not found", client.DefaultConfigFile) } log.Debug("Loading default config") return client.NewConfig(), nil } -//lint:ignore U1000 Conditionally used in different builds -func defaultClientConfigFileUnix() (string, error) { - u, err := user.Current() - if err != nil { - return "", fmt.Errorf("could not determine current user: %w", err) - } - configFile := clientRootConfigFileUnixAbsolute - if u.Uid != "0" { - homeDir, err := os.UserConfigDir() - if err != nil { - return "", fmt.Errorf("could not determine user config dir: %w", err) - } - return filepath.Join(homeDir, clientUserConfigFileUnixRelative), nil - } - return configFile, nil -} - -//lint:ignore U1000 Conditionally used in different builds -func defaultClientConfigFileWindows() (string, error) { - homeDir, err := os.UserConfigDir() - if err != nil { - return "", fmt.Errorf("could not determine user config dir: %w", err) - } - return filepath.Join(homeDir, clientUserConfigFileWindowsRelative), nil -} - func logMessagePrefix(m *client.Message) string { return fmt.Sprintf("%s/%s", util.ShortTopicURL(m.TopicURL), m.ID) } diff --git a/cmd/subscribe_darwin.go b/cmd/subscribe_darwin.go index 487f0641..00335540 100644 --- a/cmd/subscribe_darwin.go +++ b/cmd/subscribe_darwin.go @@ -1,3 +1,5 @@ +//go:build darwin + package cmd const ( @@ -10,7 +12,3 @@ or "~/Library/Application Support/ntfy/client.yml" for all other users.` var ( scriptLauncher = []string{"sh", "-c"} ) - -func defaultClientConfigFile() (string, error) { - return defaultClientConfigFileUnix() -} diff --git a/cmd/subscribe_unix.go b/cmd/subscribe_unix.go index 3f5f526f..4c9c6039 100644 --- a/cmd/subscribe_unix.go +++ b/cmd/subscribe_unix.go @@ -12,7 +12,3 @@ or ~/.config/ntfy/client.yml for all other users.` var ( scriptLauncher = []string{"sh", "-c"} ) - -func defaultClientConfigFile() (string, error) { - return defaultClientConfigFileUnix() -} diff --git a/cmd/subscribe_windows.go b/cmd/subscribe_windows.go index 22c07d81..ea5f09f0 100644 --- a/cmd/subscribe_windows.go +++ b/cmd/subscribe_windows.go @@ -1,3 +1,5 @@ +//go:build windows + package cmd const ( @@ -9,7 +11,3 @@ const ( var ( scriptLauncher = []string{"cmd.exe", "/Q", "/C"} ) - -func defaultClientConfigFile() (string, error) { - return defaultClientConfigFileWindows() -} diff --git a/docs/config.md b/docs/config.md index 584f4064..8a125146 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1060,84 +1060,6 @@ or the root domain: } ``` -## Windows service -ntfy can run as a Windows service, allowing it to start automatically on boot and run in the background. -ntfy automatically detects when it is running as a Windows service and adjusts its behavior accordingly. - -### Installing the service -To install ntfy as a Windows service, open an **Administrator** command prompt or PowerShell and run: - -```cmd -sc create ntfy binPath= "C:\path\to\ntfy.exe serve" start= auto -``` - -!!! note - Make sure to replace `C:\path\to\ntfy.exe` with the actual path to your ntfy executable. - The spaces after `binPath=` and `start=` are required. - -You can also specify a config file: - -```cmd -sc create ntfy binPath= "C:\path\to\ntfy.exe serve --config C:\ProgramData\ntfy\server.yml" start= auto -``` - -### Starting and stopping -To start the service: - -```cmd -sc start ntfy -``` - -To stop the service: - -```cmd -sc stop ntfy -``` - -To check the service status: - -```cmd -sc query ntfy -``` - -### Configuring the service -The default configuration file location on Windows is `%ProgramData%\ntfy\server.yml` (typically `C:\ProgramData\ntfy\server.yml`). -Create this directory and file manually if needed. - -Example minimal config: - -```yaml -base-url: "https://ntfy.example.com" -listen-http: ":80" -cache-file: "C:\\ProgramData\\ntfy\\cache.db" -auth-file: "C:\\ProgramData\\ntfy\\auth.db" -``` - -!!! warning - Use double backslashes (`\\`) for paths in YAML files on Windows, or use forward slashes (`/`). - -### Viewing logs -By default, ntfy logs to stderr. When running as a Windows service, you can configure logging to a file: - -```yaml -log-file: "C:\\ProgramData\\ntfy\\ntfy.log" -log-level: info -``` - -### Removing the service -To remove the ntfy service: - -```cmd -sc stop ntfy -sc delete ntfy -``` - -### Limitations on Windows -When running on Windows, the following features are not available: - -- **Unix socket listening**: The `listen-unix` option is not supported on Windows -- **Config hot-reload**: The SIGHUP signal for hot-reloading configuration is not available on Windows; restart the service to apply config changes - ## Firebase (FCM) !!! info Using Firebase is **optional** and only works if you modify and [build your own Android .apk](develop.md#android-app). diff --git a/docs/install.md b/docs/install.md index a437499e..0711fdb2 100644 --- a/docs/install.md +++ b/docs/install.md @@ -229,37 +229,28 @@ brew install ntfy ## Windows The ntfy server and CLI are fully supported on Windows. You can run the ntfy server directly or as a Windows service. +To install, you can either -To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_windows_amd64.zip), +* [Download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_windows_amd64.zip), extract it and place the `ntfy.exe` binary somewhere in your `%Path%`. +* Or install ntfy from the [Scoop](https://scoop.sh) main repository via `scoop install ntfy` -Also available in [Scoop's](https://scoop.sh) Main repository: +Once installed, you can run the ntfy CLI commands like so: -`scoop install ntfy` - -### Running the server -To run the ntfy server directly: ``` -ntfy serve +ntfy.exe -h ``` -The default configuration file location on Windows is `%ProgramData%\ntfy\server.yml` (e.g., `C:\ProgramData\ntfy\server.yml`). -You may need to create the directory and config file manually. +The default configuration file location on Windows is `%ProgramData%\ntfy\server.yml` (e.g., `C:\ProgramData\ntfy\server.yml`) +for the server, and `%AppData%\ntfy\client.yml` for the client. You may need to create the directory and config file manually. -For information on running ntfy as a Windows service, see the [Windows service](config.md#windows-service) section in the configuration documentation. +To install the ntfy server as a Windows service, you can use the built-in `sc` command. For example, run this in an +elevated command prompt (adjust the path to `ntfy.exe` accordingly): -### Client configuration -The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file). - -!!! info - There is currently no installer for Windows, and the binary is not signed. If this is desired, please create a - [GitHub issue](https://github.com/binwiederhier/ntfy/issues) to let me know. - -!!! note - Some features are not available on Windows: - - - Unix socket listening (`listen-unix`) is not supported - - Config hot-reload via SIGHUP is not available; restart the service to apply config changes +``` +sc create ntfy binPath="C:\path\to\ntfy.exe serve" start=auto +sc start ntfy +``` ## Docker The [ntfy image](https://hub.docker.com/r/binwiederhier/ntfy) is available for amd64, armv6, armv7 and arm64. It should From 64b0bd63af3a5044c1367c03c8a3ebd7a1ded09f Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 17 Jan 2026 18:05:36 -0500 Subject: [PATCH 6/6] Deps --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index beac6b8f..ed16cabc 100644 --- a/Makefile +++ b/Makefile @@ -107,6 +107,7 @@ build-deps-ubuntu: curl \ gcc-aarch64-linux-gnu \ gcc-arm-linux-gnueabi \ + gcc-mingw-w64-x86-64 \ python3 \ python3-venv \ jq