Chore management system.
In your NixOS flake:
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
chorbar.url = "github:C0DK/chorbar";
};
outputs = { self, nixpkgs, chorbar, ... }: {
nixosConfigurations.your-host = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
./your-existing-config.nix
chorbar.nixosModules.default
];
};
};
}Then:
sudo nixos-rebuild switch --flake .#your-host
curl http://localhost:8080/Point chorbar.envFile at an env file (KEY=VALUE lines) to inject it
into the chorbar-web container at boot; leave it null (the default)
to skip:
chorbar.envFile = "/run/secrets/testmaxing.env";The module is agnostic about how that file gets produced. Render it
with whatever secret manager your host config uses — sops-nix
sops.templates, agenix, a statusFile from another flake, etc. For a
sops-nix example, see the secrets section under observability below.
services.postgresql—chorbardatabase,chorbar-podlogin role, TCP enabled on 127.0.0.1 with trust auth for local connections.virtualisation.podman+ anoci-containers.chorbar-webcontainer runningchorbar:mainfrom the locally-built image tarball. Exposes port8080on the host.
The module does not open the firewall, configure SSH, set up networking, or define users — that's intentionally left to the host config that imports it.
chorbar.nixosModules.observability adds Grafana (port 3000), Loki
(log storage), Grafana Alloy (ships container logs from journald to
Loki), and Prometheus (scrapes the app's /metrics endpoint). It also
creates a grafana PostgreSQL role with read-only (SELECT) access to
the chorbar database.
Import it alongside the default module:
modules = [
./your-existing-config.nix
chorbar.nixosModules.default
chorbar.nixosModules.observability # ← add this
];sops-nix is pulled in transitively by nixosModules.observability (the
default module is secret-manager-agnostic). The age key defaults to
the host's SSH host key (/etc/ssh/ssh_host_ed25519_key, generated by
NixOS on first boot) — no age-keygen step.
One-time bootstrap (from your laptop, after first deploy):
-
Grab the host's age public key from its SSH host key:
ssh root@your-host 'cat /etc/ssh/ssh_host_ed25519_key.pub' \ | nix run nixpkgs#ssh-to-age # → age1xyz...
-
Create
secrets.yamlin your config repo, encrypted to that key:GRAFANA_KEY=$(LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c 48) nix shell nixpkgs#sops -c sops --age age1xyz... \ --set '["grafana_secret_key"] "'"$GRAFANA_KEY"'"' secrets.yaml nix shell nixpkgs#sops -c sops secrets.yaml # opens editor for more keys
Add any keys you want in your chorbar env file (e.g.
brevo_api_key) in the same file. -
Point sops-nix at the file in your host config:
sops.defaultSopsFile = ./secrets.yaml; # No sops.age.keyFile needed — SSH host key is used by default.
-
Commit
secrets.yaml(the encrypted blob) to your config repo and redeploy. From here, every change = edit + re-encrypt + rebuild. No host-side manual step ever again.
At boot sops-nix decrypts each secret to /run/secrets/<name>. Grafana
reads its key via $__file{...}. To feed the chorbar container, render
an env file from sops (e.g. via sops.templates."testmaxing.env") and
point chorbar.envFile at its .path. Rotate by editing
secrets.yaml and redeploying.
services.loki— log storage on127.0.0.1:3100(not public).services.alloy— Grafana Alloy readspodman-chorbar-web.servicejournal entries, drops human-readable lines, parses Serilog compact-JSON, and extracts@las alevellabel before shipping to Loki.services.prometheus— scrapes127.0.0.1:8080/metrics(the chorbar app) every 15s. Binds to127.0.0.1:9090only. The app emitschorbar_page_views_total{route,method,status}andchorbar_unique_visitors_total{route}(unique = sha256(ip+ua), 24h window), plus the standard prometheus-net HTTP/runtime metrics.services.grafana— listens on:3000with three pre-provisioned datasources: Loki (default), Prometheus, and Chorbar DB (read-only Postgres). Sign-up and anonymous access are disabled.sops.secrets.grafana_secret_key— declared but not encrypted by chorbar; you supply the encrypted value (see above).chorbar-grafana-db-grants— oneshot systemd service that grantsCONNECT,USAGE, andSELECTon all current and future chorbar tables to thegrafanarole.
The module does not open the firewall. Add networking.firewall. allowedTCPPorts = [ 3000 ]; in your host config if you want Grafana
reachable from outside. Loki, Prometheus, and Alloy bind to
127.0.0.1 only.
Note: the chorbar app's
/metricsendpoint is served on the same port as the rest of the app (8080). If you put chorbar behind a public reverse proxy, block/metricsthere — Prometheus scrapes over loopback and does not need it exposed publicly.
If you front chorbar with Caddy, return 404 for /metrics before
reverse-proxying the rest. Example Caddyfile:
chorbar.example.com {
@metrics path /metrics /metrics/*
respond @metrics 404
reverse_proxy 127.0.0.1:8080
}
The matcher fires before reverse_proxy, so the request never reaches
the app from the public side. Local Prometheus keeps scraping
127.0.0.1:8080/metrics directly, bypassing Caddy entirely.
- Change Grafana admin password on first login (
/profile). - Put Grafana behind a TLS reverse proxy (nginx, Caddy) and then
set
services.grafana.settings.security.cookie_secure = true.