NOMIRA
Deploy

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":

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:

  1. Repo → Settings → Pages → Build and deployment → Source: GitHub Actions.
  2. Push to main (or run "Publish site to GitHub Pages" manually under Actions).
  3. 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/.mdsite/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):

Option B — Drag-and-drop static hosts (fastest, no setup)

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:

SecretValue
VERCEL_TOKENCreate at https://vercel.com/account/tokens
VERCEL_ORG_IDorgId from .vercel/project.json
VERCEL_PROJECT_IDprojectId 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:

SecretValue
FLY_API_TOKENfly 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

SurfaceDefaultRecommended for public deploy
/ingestOPENNOMIRA_INGEST_TOKEN=… — required Authorization: Bearer …
Dashboard pagesOPENNOMIRA_DASHBOARD_PASSWORD=… — HTTP Basic in browser
Bind host127.0.0.1--host 0.0.0.0 only after the two env vars above are set
TLSn/aPut 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

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)

Quick sizing notes