Build ntfy nix container from forge mirror at v2.17.0

The nixpkgs ntfy-sh package is pinned at 2.15.0, creating a version
skew with the Dockerfile (v2.17.0). Replace the pkgs.ntfy-sh reference
with a custom derivation using fetchgit, buildNpmPackage, and
buildGoModule targeting the forge mirror. Update container-version-check
to extract versions from local nix files via regex before falling back
to the Dagger nix-version function.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Erich Blume 2026-02-20 22:18:49 -08:00
commit b98c6c1b3f
3 changed files with 85 additions and 34 deletions

View file

@ -1,20 +1,80 @@
# Nix-built ntfy push notification server
# Replaces the multi-stage Dockerfile (Node + Go + Alpine) with nixpkgs ntfy-sh
# Builds v2.17.0 from forge mirror (nixpkgs has 2.15.0)
# Built with dockerTools.buildLayeredImage for efficient layer caching
{ pkgs ? import <nixpkgs> { } }:
let
version = "2.17.0";
src = pkgs.fetchgit {
url = "https://forge.ops.eblu.me/eblume/ntfy.git";
rev = "v${version}";
hash = "sha256-/dxILAkye1HwYcybnx1WrMRK2jXZMrxal2ZKm6y2bWc=";
};
ui = pkgs.buildNpmPackage {
inherit src version;
pname = "ntfy-sh-ui";
npmDepsHash = "sha256-d73rymqCKalsjAwHSJshEovmUHJStfGt8wcZYN49sHY=";
prePatch = ''
cd web/
'';
installPhase = ''
runHook preInstall
mv build/index.html build/app.html
rm build/config.js
mkdir -p $out
mv build/ $out/site
runHook postInstall
'';
};
ntfy = pkgs.buildGoModule {
inherit src version;
pname = "ntfy-sh";
vendorHash = "sha256-/mQ+UwBYz78mPVVwYgsSYatE00ce2AKXJdx+nl6oT8E=";
doCheck = false;
ldflags = [
"-s"
"-w"
"-X main.version=${version}"
];
postPatch = ''
sed -i 's# /bin/echo# echo#' Makefile
'';
# Copy pre-built web UI; skip docs (create placeholder for go:embed)
preBuild = ''
cp -r ${ui}/site/ server/
mkdir -p server/docs && touch server/docs/placeholder
'';
meta = with pkgs.lib; {
description = "Send push notifications to your phone or desktop via PUT/POST";
homepage = "https://ntfy.sh";
license = licenses.asl20;
mainProgram = "ntfy";
};
};
in
pkgs.dockerTools.buildLayeredImage {
name = "blumeops/ntfy";
tag = "latest";
contents = [
pkgs.ntfy-sh
ntfy
pkgs.cacert
pkgs.tzdata
];
config = {
Entrypoint = [ "${pkgs.ntfy-sh}/bin/ntfy" ];
Entrypoint = [ "${ntfy}/bin/ntfy" ];
Env = [
"SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
"TZDIR=${pkgs.tzdata}/share/zoneinfo"

View file

@ -1,7 +1,6 @@
---
title: Fix ntfy Nix Version
modified: 2026-02-20
status: active
tags:
- how-to
- containers
@ -17,40 +16,24 @@ Override the nixpkgs ntfy-sh derivation to build v2.17.0 from the forge mirror,
Discovered during [[add-container-version-sync-check]]: the ntfy container has both a Dockerfile and a `default.nix`. The Dockerfile builds v2.17.0 from `forge.ops.eblu.me/eblume/ntfy.git`, but the nix derivation uses `pkgs.ntfy-sh` from nixpkgs which is pinned at 2.15.0. The version sync check currently excludes ntfy from nix version validation as a workaround.
## What to Do
## What Was Done
Override the nixpkgs `ntfy-sh` derivation in `containers/ntfy/default.nix` to build from the forge mirror at the v2.17.0 tag. The nixpkgs derivation uses `buildGoModule` with a nested `buildNpmPackage` for the web UI.
Replaced the nixpkgs `pkgs.ntfy-sh` reference in `containers/ntfy/default.nix` with a custom derivation that builds v2.17.0 from the forge mirror using `fetchgit`, `buildNpmPackage` (web UI), and `buildGoModule` (server). Docs are skipped (placeholder for `go:embed`, matching the Dockerfile approach).
### Hashes to Update
| Hash | What it covers | When to update |
|------|---------------|----------------|
| `src.hash` | Source tarball integrity | Always (new source) |
| `vendorHash` | Go module dependencies | If `go.mod`/`go.sum` changed between 2.15.0 and 2.17.0 |
| `npmDepsHash` | npm dependencies | If `web/package-lock.json` changed |
Use `lib.fakeHash` for each, attempt a build, and nix will report the expected hash.
### Steps
1. In `containers/ntfy/default.nix`, override `pkgs.ntfy-sh` with `fetchgit` pointing to `https://forge.ops.eblu.me/eblume/ntfy.git` at the v2.17.0 tag
2. Update all three hashes via iterative builds
3. Build and test with `dagger call build-nix --src=. --container-name=ntfy`
4. Re-enable ntfy in `NIX_PACKAGE_MAP` in `mise-tasks/container-version-check`
5. Verify `mise run container-version-check --all-files` passes
The `container-version-check` script was updated to extract versions from local nix files via regex (`version = "X.Y.Z"`) before falling back to the Dagger `nix-version` function for unmodified nixpkgs packages. This avoids the issue where `nix eval nixpkgs#ntfy-sh.version` returns the upstream 2.15.0 instead of our overridden 2.17.0.
## Key Files
| File | Change |
|------|--------|
| `containers/ntfy/default.nix` | Override ntfy-sh derivation to build from forge |
| `mise-tasks/container-version-check` | Re-add ntfy to `NIX_PACKAGE_MAP` |
| `containers/ntfy/default.nix` | Custom derivation building v2.17.0 from forge |
| `mise-tasks/container-version-check` | Regex-based local nix version extraction |
## Verification
- [ ] `dagger call build-nix --src=. --container-name=ntfy` produces a working image
- [ ] `dagger call nix-version --package=ntfy-sh` returns 2.17.0 (or the overridden version is extractable)
- [ ] `mise run container-version-check --all-files` passes with ntfy included
- [x] `dagger call build-nix --src=. --container-name=ntfy` produces a working image
- [x] Version extractable from local `default.nix` via regex (2.17.0)
- [x] `mise run container-version-check --all-files` passes with ntfy included
## Related

View file

@ -45,14 +45,16 @@ CONTAINER_TO_SERVICE = {
"kiwix-serve": "kiwix",
}
# Container dir name → nixpkgs package name for dagger nix-version
# ntfy excluded until nix derivation is updated to build v2.17.0 from forge
# See: docs/how-to/zot/fix-ntfy-nix-version.md
# Container dir name → nixpkgs package name for dagger nix-version.
# Used for containers that use an unmodified nixpkgs package (version matches upstream).
# Containers with local overrides (e.g. ntfy) declare version in default.nix
# and are detected automatically via NIX_VERSION_PATTERN.
NIX_PACKAGE_MAP = {
"authentik": "authentik",
}
VERSION_ARG_PATTERN = re.compile(r"^ARG\s+CONTAINER_APP_VERSION=(\S+)", re.MULTILINE)
NIX_VERSION_PATTERN = re.compile(r'^\s*version\s*=\s*"([^"]+)"\s*;', re.MULTILINE)
app = typer.Typer()
console = Console()
@ -91,8 +93,14 @@ def changed_containers() -> set[str] | None:
return names
def get_nix_version(container_name: str) -> str | None:
"""Extract nix package version via dagger. Returns None if unavailable."""
def get_nix_version(container_name: str, nix_file: Path) -> str | None:
"""Extract nix package version. Tries local nix file first, then dagger."""
# Try extracting version declared directly in the nix file (local overrides)
match = NIX_VERSION_PATTERN.search(nix_file.read_text())
if match:
return match.group(1)
# Fall back to dagger for unmodified nixpkgs packages
pkg = NIX_PACKAGE_MAP.get(container_name)
if pkg is None:
return None
@ -169,7 +177,7 @@ def main(
# Rule 2: nix derivation must produce a version
if has_nix:
nix_ver = get_nix_version(name)
nix_ver = get_nix_version(name, nix_file)
if nix_ver is not None:
versions["nix"] = nix_ver
elif name in NIX_PACKAGE_MAP: