github getnora-io/nora v1.3.3

2 hours ago

[1.3.3] - 2026-10-04

Changed

  • One upstream fetch per Docker layer, however many clients ask for it (#1003) — a cold blob was fetched from upstream once per request: every concurrent pull, every client retry and every other node asking while the first fetch was still running opened its own upstream download and spooled its own copy, so a CI fleet or a node pool rolling one image multiplied upstream traffic by the number of clients (one 1.27 GB layer was fetched 56 times in four days). Now the first request leads the fill and every request for the same blob that arrives before it is stored joins it, streaming the same spool as it grows — including a Range resume, answered 206 from the shared fill. A joiner still passes its own authentication, curation, internal-namespace and quarantine checks before it joins, and only joins a fill from the upstream its own request resolved to. A fill that fails, is poisoned or panics releases the blob at once: clients already following it see the body abort (never a truncated success), and the next request starts a fresh fetch. The existing single-flight for npm metadata is unchanged; nora_proxy_coalesced_total{registry="docker"} counts the joins.
  • Package file modes no longer depend on the machine that built them (#1031) — dist/nora.nfpm.yaml left the unit file, the shipped nora.env and the copyright at whatever mode the build tree had, so a builder with umask 002 published them group-writable (0664). The modes are now explicit: 0644 for the unit and the copyright, and 0640 root:nora for /etc/nora/nora.env, which holds tokens and S3 credentials and is read by systemd as an EnvironmentFile before the service drops to the nora user — it never needed to be world-readable. The packages are also installed and upgraded in CI now, in a container with systemd as PID 1, which is how the mode drift was noticed in the first place.
  • A raw upload answers 200, not 201 (#1039) — a PUT that created a new object answered 201 Created, and object-storage clients read anything but 200 as a failure: lake cache put aborts with error: failed to upload artifact, error 201 even though NORA has stored the object, which is the worst shape of a failure. raw is the surface such clients reach for — S3's own PutObject answers 200 — so every successful PUT (a create, an If-Match overwrite, or a re-PUT of identical bytes) now answers 200 and the OpenAPI document lists one success code instead of two. A client that treats any 2xx as success sees no difference; one that asserted exactly 201 has to accept 200.

Fixed

  • A large Docker layer pulls through a slow or flaky upstream link (#1015) — proxy_timeout was applied as a total deadline on the upstream blob request, so any layer that took longer than proxy_timeout to transfer was cut mid-body even while bytes kept flowing (a 3.5 GB layer with proxy_timeout = 120 failed every attempt), and a body that broke off was discarded and fetched again from byte zero. proxy_timeout now bounds only the wait for upstream response headers; the body is paced by the per-chunk read_timeout. A body that breaks off (connection reset, stalled chunk, short body) is resumed from upstream with Range: bytes=<written>- into the same spool and the same digest check, up to five attempts in a row without progress; an upstream that ignores Range and resends the whole blob has the bytes already on disk skipped. A client Range on a cache miss — how containerd and dockerd resume a broken pull — is now answered 206 from the spool (or 416 past the end) instead of 200 from byte zero, which made the client read and discard everything up to its offset (failed to discard to offset). An upstream that refuses a resume (4xx, or a token it no longer accepts) is recorded as alive, not failed, by the circuit breaker, as on the initial request.
  • Graceful shutdown drains for a bounded time (#1016) — on SIGTERM/SIGINT NORA stopped accepting connections and then waited for every open one with no limit, so a client that stopped reading a response (dockerd after a failed layer, a stalled proxy) held the process until the orchestrator killed it, and the steps after serving — the audit log drain and the token last_used flush — never ran. In-flight requests now get server.shutdown_timeout seconds (NORA_SHUTDOWN_TIMEOUT, default 15, which leaves room for the scheduler wait inside Kubernetes' default 30 s grace period); connections still open then are closed and shutdown continues.
  • Concurrent cross-device writes of one blob no longer collide (#1019) — when a proxied blob's spool and local storage sit on different filesystems, put_from_path copies through a temp file, and that temp file was always <blob>.tmp. Two writes of the same blob at once (two clients pulling one cold layer) truncated each other's copy, a reader could briefly open the blob shorter than its content, and the second write failed with NotFound because its temp file had been renamed away. Each write now stages its own copy under a unique name, the same way put() already did, so every concurrent write succeeds and only a complete file is ever published.
  • The systemd unit ships under /usr/lib, not the aliased /lib (#1023) — Debian policy has forbidden /lib/systemd/system since the /usr merge, and that is where the packaged unit went: four aliased-location lintian errors in every release, and an install path that is a symlink into /usr on any current system. Both the .deb and the .rpm now place it at /usr/lib/systemd/system/nora.service, where systemd looks for it, and dpkg -L lists only that path. Fixed by @nicholas-rees.
  • A fresh package install starts instead of crash-looping (#1025) — the .deb/.rpm and install.sh defaults set NORA_HOST=0.0.0.0 with no NORA_PUBLIC_URL, which validation rejects, so the service panicked (SIGABRT) and systemd restarted it every 5 s forever. The shipped defaults are now NORA_HOST=:: (IPv4 and IPv6, falling back to IPv4 where IPv6 is unavailable) with NORA_PUBLIC_URL=http://localhost:4000, the same as the Docker image; set your real address for clients on other hosts. A configuration NORA cannot start with now exits with status 78 (EX_CONFIG) and a clear message instead of panicking, and the unit files carry RestartPreventExitStatus=78, so an existing install whose nora.env still has the old defaults stops once with the reason in the journal rather than looping.
  • An upgrade stopped the registry and never started it again (#1035) — apt upgrade nora and dnf upgrade nora reported success while leaving the service down until somebody ran systemctl start nora. Both maintainer scripts ignored the argument that separates an upgrade from a removal: prerm stopped the unit and dropped its enablement on every upgrade, and postinst re-enabled it but started nothing. An upgrade that aborted before postinst — a non-interactive dpkg stopping at an edited nora.env conffile prompt — left the unit both stopped and disabled, so not even a reboot brought it back, and under unattended-upgrades the registry went down at night with client failures as the first signal. prerm now stops and disables only on a real removal (dpkg's remove, rpm's last instance), postinst runs systemctl try-restart on an upgrade so a unit the administrator had stopped stays stopped, and both tolerate a system without systemd instead of failing the install. The behaviour is covered by scripts/test-package-scriptlets.sh in CI, which runs the real scripts against a stub systemctl and asserts which verbs they invoke.
  • raw rejected a retried upload of unchanged bytes (#1036) — a PUT with no conditional headers to a key that already held identical content answered 409 File already exists, the same as a genuine conflict. Object-storage clients that retry an upload after a partial transfer expect a retry of the same bytes to succeed, not fail. The unconditional PUT path now compares the uploaded content's sha-256 against the stored object's pin (the same lookup already used for If-Match) and treats a match as success instead of a conflict. Different bytes under an existing key are unaffected — immutability still holds, and still answers 409.
  • A false INTEGRITY VIOLATION on the local backend (#1041) — the body of an object and its hash pin are written as two separate steps (a rename, then an append to the NDJSON pin sidecar), so two writers of one key could interleave into "body from one writer, last pin from the other". The next read then failed hash-pin verification and refused to serve bytes nobody had tampered with: INTEGRITY VIOLATION: refusing to serve tampered artifact. It surfaced on a Docker pull-through cache, where a manifest's .meta.json is written both by the pull counter and by the proxy-cache fill — one key out of 4039 failed a nora migrate, and the same read path serves client pulls, so that manifest's metadata was unreadable until the key was rewritten. Both durable steps now happen inside a per-key critical section in the backend itself, so no writer can produce the split, whether or not it takes a publish lock. The object-store backends were never affected: there the pin travels in the object's own metadata, written in the same request as the body. A crash between the two steps still leaves a stale pin behind, which an operator repairs by rewriting the key — the body is intact.

Install

# x86_64
curl -LO https://github.com/getnora-io/nora/releases/download/v1.3.3/nora-linux-amd64
chmod +x nora-linux-amd64
sudo mv nora-linux-amd64 /usr/local/bin/nora

# ARM64 (Apple Silicon, Graviton, Ampere)
curl -LO https://github.com/getnora-io/nora/releases/download/v1.3.3/nora-linux-arm64
chmod +x nora-linux-arm64
sudo mv nora-linux-arm64 /usr/local/bin/nora

Docker

docker pull getnora/nora:1.3.3
Variant Image Platforms
Alpine (default) getnora/nora:1.3.3 amd64, arm64
RED OS getnora/nora:1.3.3-redos amd64
Astra Linux SE getnora/nora:1.3.3-astra amd64
GHCR ghcr.io/getnora-io/nora:1.3.3 amd64, arm64

DEB / RPM

# Debian / Ubuntu / Astra Linux (amd64)
curl -LO https://github.com/getnora-io/nora/releases/download/v1.3.3/nora-amd64.deb
sudo dpkg -i nora-amd64.deb

# RHEL / Fedora / RED OS (amd64)
curl -LO https://github.com/getnora-io/nora/releases/download/v1.3.3/nora-amd64.rpm
sudo rpm -i nora-amd64.rpm

Changelog

See CHANGELOG.md

Don't miss a new nora release

NewReleases is sending notifications on new releases.