- Rust 60.7%
- Nix 31%
- Shell 8.3%
Odyssey's Firecracker image is Alpine on a read-only root with no xz, no useradd, a 64 MiB /tmp, the only real disk at /workspace, and no devpts mounted. Each of those breaks a different part of the obvious 'install nix and build' path, and the runner caps how long a single command line can be, so the workarounds cannot live in the command box. The script mounts devpts (nix needs a pty to capture builder output), points TMPDIR at the big volume, then picks the best available store: nix on PATH, else a native install if /nix can be a mountpoint, else nix-portable with bwrap in preference to proot. POSIX sh, verified against dash and busybox ash and clean under shellcheck -s sh. |
||
|---|---|---|
| docs | ||
| hosts/noisebell-do | ||
| pi | ||
| remote | ||
| scripts | ||
| secrets | ||
| .envrc | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
| rustfmt.toml | ||
Noisebell
Monitors the door at Noisebridge and tells you whether it's open or closed.
A Raspberry Pi reads a magnetic sensor on the door and pushes state changes to a cache server. The cache keeps the latest state and fans updates out to chat integrations such as Discord and Zulip.
Pi (door sensor) ──webhook──> Cache ──webhook──> Discord / Zulip
|
polls Pi <-+
|
+──webhook──> Pi relay ──webhook──> Home Assistant
Layout
| Directory | What it is |
|---|---|
pi/ |
Raspberry Pi OS base with laptop-built Noisebell deploy |
remote/ |
Server-side services (cache, RSS, Discord, Zulip) |
hosts/noisebell-do/ |
Standalone DigitalOcean NixOS host for the remote services |
secrets/ |
Shared agenix-encrypted secrets and recipient rules |
Each directory has its own README with setup and configuration details.
For hosted deployment, this repo exports nixosConfigurations.noisebell-do, a small DigitalOcean NixOS host that imports noisebell.nixosModules.default. The host provides deployment-specific values like domains and the Pi address, while the Noisebell module itself points agenix at the encrypted files in secrets/ and consumes the decrypted runtime files on the target machine.
Useful commands:
./scripts/deploy-do [jet@noisebell-do]redeploys the DigitalOcean remote host./scripts/nhsredeploys the old Hetzner host using the local checkout as the flake inputscripts/deploy-pios-pi.sh pi@100.66.45.36redeploys the Raspberry Pi OS machinescripts/share-grafana-public-dashboard jet@noisebell-dorepairs or prints the deterministic public-safe Grafana dashboard link
The full Home Assistant relay workflow is documented in pi/README.md. Public hosting, Cloudflare Tunnel, firewall, and Grafana sharing details are documented in docs/hosting.md.
CI
Everything CI gates on is a Nix derivation, so a runner needs Nix and nothing else — no
Rust install, no cross toolchain setup, no cargo invocations of its own.
nix build -L --max-jobs auto --cores 0 .#ci
.#ci is a link farm over every entry in checks.x86_64-linux, which is what makes the
run parallel: Nix sees the checks as independent derivations and schedules them across all
available cores at once, rather than running fmt, then clippy, then tests, then builds.
| Check | What it covers |
|---|---|
fmt |
cargo fmt --check |
clippy |
--all-targets with --deny warnings |
nextest |
the workspace test suite |
noisebell-{cache,rss,discord,zulip} |
x86_64 release binaries |
noisebell-pi{,-relay} |
aarch64 glibc, cross-compiled |
noisebell-pi{,-relay}-static |
aarch64 musl, statically linked |
Individual checks build on their own, which is the faster loop when iterating:
nix build -L --max-jobs auto --cores 0 \
.#checks.x86_64-linux.clippy .#checks.x86_64-linux.nextest
nix flake check also works and additionally evaluates the NixOS configurations, but it
skips the aarch64-linux outputs, so it is the slower and less complete of the two.
Toolchain
The flake pins a nightly toolchain via selectLatestNightlyWith, which walks back from the
newest nightly until it finds one that ships every component the build asks for — a
partially published nightly cannot break CI. Nightly buys two things:
-Z threadsparallelises the rustc frontend within a single crate. Cargo already builds independent crates in parallel; this fills in the tail of the dependency graph, where a workspace this narrow otherwise compiles one crate on one core.- Cranelift (
rustc-codegen-cranelift-preview) replaces LLVM as the codegen backend for the dev profile only. It emits object code much faster and the code it emits is slower, which is the right trade for clippy and tests. Release binaries — the ones that actually ship to the Pi and the DO host — stay on LLVM.
Cross-compilation for both Pi targets comes from the same toolchain carrying the aarch64
std; only the C toolchain and the cargo target environment differ between the glibc and
musl variants. Each target does one shared dependency build for both of its binaries, and
that build is scoped to -p noisebell -p noisebell-relay so host-only crates never enter
the cross graph.
Runner sizing
The build closure is 1207 store paths totalling 4.1 GB, most of it the nightly toolchain,
the aarch64 cross toolchains, and vendored crates. Roughly 0.4 GB of that substitutes
straight from cache.nixos.org (including both cross C toolchains, so there is no gcc
bootstrap); another ~0.2 GB is Rust nightly tarballs from static.rust-lang.org that are
unpacked and patchelfed locally.
A disposable VM therefore wants ~16 GB of disk — closure plus cargo target directories
during the build — and 16 GB of RAM if you run with --max-jobs auto --cores 0, since that
can put several dependency builds in flight at once with a full complement of rustc
processes in each. Halve --max-jobs if memory is tighter than that.
Every run from a fresh VM redoes all of it. If this runs often, pointing the runner at a
binary cache (attic, cachix) or giving it a persistent /nix turns most of the work into a
download, and an unchanged tree into a no-op.
Observability
The DigitalOcean host runs Prometheus, Loki, Grafana, Alloy, node_exporter, and blackbox_exporter via hosts/noisebell-do/observability.nix. Grafana provisions Noisebell Full Debug for authenticated operators and Noisebell Public for externally shared, Prometheus-only status.
- Grafana:
https://grafana-noisebell.extremist.software/through Cloudflare Tunnel, login required - Public-safe Grafana dashboard:
https://grafana-noisebell.extremist.software/public-dashboards/6e6f69736562656c6c7075626c696330 - Prometheus:
http://noisebell-do:9090/over Tailscale - Loki:
http://noisebell-do:3100/over Tailscale
The Pi deploy script enables persistent journald, installs prometheus-node-exporter, and installs noisebell-loki-journal.service to ship Pi journal logs to Loki on the DO host.
Prometheus is the source of truth for regular time-based data: scrape health, host CPU/memory/disk/uptime, DO-to-Pi poll counts and last results, GPIO state, Pi hardware readings, webhook counters, and retry counters. Loki/journald is reserved for sparse event logs that should be readable in chronological order: service start/stop, door state changes, cache state changes, Pi offline/online transitions, auth or rate-limit rejections, webhook retries/failures, stale events, and GPIO read errors. Routine successful polls, unchanged poll results, metrics scrapes, and badge/image/status reads are intentionally not logged at INFO.