Deployment Guide
Individual, small team, your own server, public web hosting.
Nomira is open-source and self-hosted by design — not a multi-tenant SaaS. That positioning is the moat against closed FinOps tools and is what lets us honestly promise "content never leaves your machine." This guide covers every realistic path from "I want to try it" to "my team uses it daily."
Plain-English summary. The CLI runs on each developer's machine
(pip install nomira). The dashboard is installed once by one person —
on a laptop, a VM, or a small container host. Teammates don't install the
dashboard; they just ship usage to it.
Onboarding scenarios
1. Solo / individual developer (≈ 30 seconds)
pip install nomira # stdlib-only — no transitive deps
nomira # analyze your newest Claude Code session
nomira --all # the whole portfolio
nomira --source codex # Codex sessions
Reads logs you already have on disk; nothing leaves your machine. No account, no server.
2. Small team (one shared dashboard) (≈ 5 minutes)
One person — call them the team admin — runs the dashboard on any Docker host (their laptop on the VPN, a small VM, an internal server, or Fly.io / Render / Railway). Everyone else just ships from their machine.
Admin (once):
git clone …/smartokens && cd smartokens
echo "NOMIRA_DASHBOARD_PASSWORD=$(openssl rand -hex 12)" > .env
docker compose up -d
# mint a per-tenant API key for each team
docker compose exec nomira nomira --create-tenant acme --db /data/usage.db
# → prints "nm_..." once; hand it to the team
For Fly.io / Render / Cloud Run instead of Docker on your own VM, see the "Fly.io recipe" below — same idea, faster setup.
Each developer (once):
pip install nomira
nomira --ship --remote https://your-host/ingest --token nm_acme_xxx --developer "$(whoami)"
# rerun any time, or wire as a daily cron / launchd / scheduled task
That's it. Devs never install the dashboard — only the ~150 KB CLI.
Privacy-paranoid teams can skip the live connection entirely:
nomira --export events.json --tenant acme
# upload events.json at https://your-host/import — no live machine→server connection ever
3. SDK in your own product (≈ 10 lines)
Use the SDK to track your own app's API calls (not coding-assistant logs):
import nomira
nomira.configure(remote="https://your-host/ingest", token="<team token>")
resp = client.messages.create(...)
nomira.track(model="claude-opus-4-7", usage=resp.usage,
feature="doc-summary", workflow="onboarding", user_tier="free")
Privacy invariant: counts and tags only, never prompt/response content.
Hosting the dashboard
Docker (recommended)
docker compose up -d # uses Dockerfile + docker-compose.yml
# data persists in ./data (SQLite store)
Or directly:
docker build -t nomira .
docker run -d -p 8787:8787 -v $PWD/data:/data \
-e NOMIRA_INGEST_TOKEN=… -e NOMIRA_DASHBOARD_PASSWORD=… \
nomira
Bare-metal / VM (no Docker)
pip install nomira
export NOMIRA_INGEST_TOKEN=…
export NOMIRA_DASHBOARD_PASSWORD=…
nomira --serve --host 0.0.0.0 --port 8787 --db /var/lib/nomira/usage.db
Wrap as a systemd service for restart-on-boot.
"Free public host" recipes (managed Docker)
The Dockerfile is the universal target — these all "just work":
- Render / Railway — point at the repo; set env vars; mount a persistent disk at
/data - Cloud Run —
gcloud run deploy --source .(note: ephemeral disk; mount Cloud Storage if you want persistence) - A small EC2/Hetzner/DigitalOcean VM —
docker compose up -dand a Caddy/Nginx in front
Fly.io — the most concrete recipe (recommended for first design partners)
A fly.toml ships in the repo. From the admin's machine:
# one-time setup (~5 min)
fly auth login # interactive
fly launch --no-deploy --name nomira-dashboard --region iad # creates the app
fly volumes create nomira_data --region iad --size 1 # persistent SQLite (1 GB)
fly secrets set NOMIRA_DASHBOARD_PASSWORD=$(openssl rand -hex 12) # HTTP Basic on dashboard
fly secrets set NOMIRA_INGEST_TOKEN=$(openssl rand -hex 16) # OPTIONAL — per-tenant API keys are preferred
fly deploy # builds the Dockerfile, ships it
# you now have https://nomira-dashboard.fly.dev — change `--name` for a different slug
Add a custom domain (e.g., app.nomiraai.com):
fly certs create app.nomiraai.com
# add the CNAME record fly prints at your DNS provider — fly auto-issues TLS
Mint per-tenant API keys (one per design partner team):
fly ssh console -C "nomira --create-tenant acme --db /data/usage.db"
# prints the key once; hand it to the team admin
Their team's CLI side then just ships:
pip install nomira
nomira --ship --remote https://app.nomiraai.com/ingest --token nm_acme_xxx --developer "$(whoami)"
Cost: ~$0–5/mo. The machine sleeps when idle (auto_stop_machines = "stop") and starts on the first request. Add machines only as load grows.
Updating: any code push → fly deploy (or wire a GitHub Action). Volume + secrets persist across deploys.
Hosting the public site (landing + deck + docs)
The site/ directory is fully static and self-contained — all links are relative. Drop it on any static host. It is NOT the team dashboard (that's the Python server). Marketing/deck are a separate public surface, usually on a different subdomain (e.g. www.nomiraai.com for marketing, app.nomira.team for the dashboard).
Option A — GitHub Pages (recommended; matches the OSS thesis)
A workflow is included at .github/workflows/pages.yml. After pushing the repo:
- Repo → Settings → Pages → Build and deployment → Source: GitHub Actions.
- Push to
main(or run "Publish site to GitHub Pages" manually under Actions). - The site goes live at
https://<org-or-user>.github.io/<repo>/within ~1 minute.
What the workflow does: checks out, runs python tools/build_docs.py to render docs/.md → site/docs/.html, then publishes the whole site/ folder to Pages. The deck is at /deck.html, docs at /docs/, landing at /.
Custom domain (e.g. nomiraai.com):
- Add a
CNAMEfile tosite/containingnomiraai.com. - Point your DNS
A(orALIAS) record at GitHub Pages IPs. - Repo → Settings → Pages → Custom domain →
nomiraai.com→ enable HTTPS.
Option B — Drag-and-drop static hosts (fastest, no setup)
- Netlify Drop — go to
app.netlify.com/drop, drag thesite/folder, done. URL in ~10 seconds. - Vercel —
npx vercel deploy site --prod(vercel loginonce); pick a project name. Custom domain in the Vercel dashboard. - Cloudflare Pages — connect the repo or drag
site/via Wrangler.
These are zero-config and free for the volume a deck attracts.
Option C — Your own server (Caddy / Nginx)
If you're already running the Nomira dashboard on a VM, host the marketing site on a different subdomain on the same machine so they don't share /:
www.nomiraai.com {
root * /srv/nomira/site
file_server
}
app.nomira.team {
reverse_proxy 127.0.0.1:8787
}
Caddy auto-issues TLS for both names.
Just the deck (one file)
Want only the deck publicly accessible? Copy site/deck.html to any host and link to it. It's self-contained — no external CSS, no fonts, no JS framework. Print to PDF (browser → Print) for an email-able copy.
Continuous deployment (GitHub Actions)
Two workflows ship in .github/workflows/:
deploy-site.yml — pushes to main that touch site/, docs/, the renderers, or vercel.json auto-rebuild the wiki/demo HTML and deploy to Vercel. Required repo secrets:
| Secret | Value |
|---|---|
VERCEL_TOKEN | Create at https://vercel.com/account/tokens |
VERCEL_ORG_ID | orgId from .vercel/project.json |
VERCEL_PROJECT_ID | projectId from .vercel/project.json |
Set them with:
gh secret set VERCEL_TOKEN
gh secret set VERCEL_ORG_ID -b "$(jq -r .orgId .vercel/project.json)"
gh secret set VERCEL_PROJECT_ID -b "$(jq -r .projectId .vercel/project.json)"
deploy-dashboard.yml — manual trigger by default (Actions tab → Run workflow). Flip the push: block in the YAML to auto-deploy on every commit that touches nomira/, Dockerfile, or fly.toml. Required repo secret:
| Secret | Value |
|---|---|
FLY_API_TOKEN | fly auth token on your machine |
gh secret set FLY_API_TOKEN -b "$(fly auth token)"
Both workflows use concurrency groups so simultaneous pushes don't race each other into the same deploy lane.
Security & access control
| Surface | Default | Recommended for public deploy |
|---|---|---|
/ingest | OPEN | NOMIRA_INGEST_TOKEN=… — required Authorization: Bearer … |
| Dashboard pages | OPEN | NOMIRA_DASHBOARD_PASSWORD=… — HTTP Basic in browser |
| Bind host | 127.0.0.1 | --host 0.0.0.0 only after the two env vars above are set |
| TLS | n/a | Put Caddy or Nginx in front (auto-HTTPS via Let's Encrypt) |
A minimal Caddyfile example:
nomira.team.example.com {
reverse_proxy 127.0.0.1:8787
}
Caddy auto-provisions TLS and adds HSTS by default.
The startup banner prints a WARNING if you bind non-localhost without the dashboard password set, so you can't forget by accident.
Data layout
- Store: SQLite at
NOMIRA_DB(default~/.nomira/usage.db, or/data/usage.dbin Docker). - Schema:
events(no content columns — content can't get in),bills(the - Prices: cached at
~/.nomira/prices_cache.json, refreshed daily. - Overrides:
~/.nomira/prices.local.jsonand~/.nomira/plan.json.
true source: provider invoices).
The container ships nothing else: no cookies, no telemetry, no phone-home.
Operations
Reconciliation (the true source)
# CSV from the provider console:
nomira --reconcile-import path/to/anthropic-export.csv
# Or the Cost API directly:
export NOMIRA_ANTHROPIC_ADMIN_KEY=sk-ant-admin-…
nomira --reconcile-fetch
Then open /reconcile for a daily Computed-vs-Billed comparison (and a delta %).
Backup
# SQLite is a single file — back it up while the server is quiescent:
docker compose exec nomira sqlite3 /data/usage.db ".backup '/data/usage.backup'"
Upgrade
docker compose pull && docker compose up -d # if pulling a tagged image
# or
git pull && docker compose build && docker compose up -d
What this is NOT (and why)
- Not a multi-tenant managed SaaS (yet). That was deliberately deferred
- Not a proxy/gateway. It does not sit in your request path. No latency tax.
- Not a place that ever stores prompt/response content — by schema design.
("no commercial feature before 10 teams ask"). The trusted-auditor positioning depends on data not crossing third-party boundaries — going SaaS first would erase the differentiator vs CloudZero/Finout/AI Vyuh.
Quick sizing notes
- The SQLite store handles ~100k events/min easily on a small VM; in real-world
- Memory: ~30–60 MB. CPU: negligible at rest, brief spikes on optimizer drill-down.
- One small VM (1 vCPU, 1 GB) is more than enough for a team of 50.
usage you'll be 100×–1000× under that.