Reading map: Demo showcase · Testing auth flows · Deployment · Troubleshooting install
Local Development with Docker
This recipe is for working on Sigra (or evaluating it deeply) on your own machine: running the test suite, booting the seeded demo app, and reaching it at a stable URL — all backed by Docker so it never fights your system Postgres or another project's ports. If you are running many Elixir apps at once (each with its own admin UI), this is the workflow that keeps them out of each other's way. Budget: under 15 minutes.
If you just want to install Sigra into your own app, you want Installation instead — this page is about the Sigra repo's own dev/demo environment.
Gameplan (TL;DR)
- Tests:
scripts/db/up.sh→direnv allow(once) →mix test. A throwaway Postgres on a random port; nothing to configure. - Demo app:
scripts/uat/up.sh— one command. It builds + boots the demo behind the shared Traefik proxy with live reload, waits until the app actually responds, auto-opens/demo/credentials, and prints grouped auth/admin/ops routes. No second terminal. - Read the printout. Both scripts print the exact URLs, routes, and commands to copy-paste.
- Iterate. Save a template or
lib/source and it hot-reloads in the running container (bind-mount + inotify) — no image rebuild, and style edits never re-download deps. Prefer the raw host-run path? Use--dev. - Tear down:
scripts/db/down.shandscripts/uat/down.sh.
What this gives you / what it doesn't
- ✅ Postgres in Docker on a dynamic port — never reserves
5432, never collides with Homebrew Postgres or a sibling project. - ✅ A stable, collision-free URL per checkout/branch, with
http://sigra.localhostas a friendly alias for your primary checkout. - ✅ A Docker image whose layers cache correctly — editing a template doesn't re-fetch or recompile dependencies.
- ❌ Not a production setup. For the prod release image and deploy config see Deployment.
Prerequisites
- Docker Desktop running.
- Elixir 1.19+ / OTP 27+ (host-run mode and
mix testrun on the host). - A modern browser. Chrome/Edge resolve
*.localhostautomatically; for Firefox/Safari/curluse the printed raw127.0.0.1:<port>URL. - Optional but recommended: direnv for zero-step test-DB discovery (a
sourcefallback is always printed). - For the install golden tests only: the pinned
phx_newarchive —mix archive.install --force hex phx_new 1.8.8.
Postgres for mix test
mix test needs a Postgres with postgres/postgres and a sigra_test database. Boot a disposable one in Docker:
scripts/db/up.sh # ephemeral Postgres on a dynamic port; writes tmp/db.env
direnv allow # one-time; auto-loads tmp/db.env on every cd into the repo
mix test
No direnv? Use the printed fallback instead:
source tmp/db.env && mix test
How it works: the container publishes Postgres on a random host port (so it can't clash with anything), scripts/db/up.sh discovers that port and writes it to tmp/db.env in both naming conventions — SIGRA_TEST_PG_* (read by the library suite) and PG* (read by the demo app and the install fixtures). When tmp/db.env isn't loaded (CI, or a plain Postgres on 5432), everything falls back to localhost:5432, so nothing breaks. The container is ephemeral (no data volume) and raised to max_connections=200 for the install suite's many generated apps. Tear it down with scripts/db/down.sh.
This test database is separate from the demo database below. Keep them apart: the demo DB persists seeded personas; the test DB is disposable.
PR-fast vs nightly CI split
A clean local mix test plus the PR-fast gate (the 5 required lanes + install_golden_contract + library_tests_dep_off) is the fast feedback loop; exhaustive broad coverage (install matrix ×4, upgrade smoke, generated-admin Playwright ~60 min) now runs on a nightly schedule/main push, not on every PR.
Reproducing the CI dep-off lane locally
The CI dep-off lane verifies that Sigra compiles and the core guard tests pass when the optional :threadline dep is absent. Reproduce it locally with a single command:
MIX_ENV=test mix sigra.dep_off
This alias (in mix.exs) runs four steps in order:
deps.unlock threadline— removes the lockfile entry for:threadline.deps.clean threadline --build— clears cached BEAM files for:threadline.compile --warnings-as-errors --no-deps-check— proves the library compiles cleanly without the dep (the D-09 compile-proof gate).test --only threadline_guard --no-deps-check— runs only the modules tagged@moduletag :threadline_guard, which cover all dep-off guard paths (D-10/D-11 coverage selection).
This requires the same live test Postgres as the full suite (boot one via scripts/db/up.sh if needed). After running mix sigra.dep_off, restore the dep with mix deps.get.
Larger-runner measurement gate (CACHE-03)
GitHub's standard ubuntu-latest runner (2 vCPU, 7 GB RAM) is free and unlimited on public repos. Larger runners (4/8/16/32 vCPU) are billed per-minute even on public repos and do not consume included minutes — they bypass the free tier entirely. At the time of writing (June 2026, GitHub Docs), a 4-core larger runner costs ~$0.016/min, while two standard runners share zero cost.
The default posture is: stay on ubuntu-latest. Do not adopt a larger runner.
Decision rule (D-22)
Adopt a larger runner for a specific job ONLY if ALL of the following are true:
| Criterion | Test |
|---|---|
| The job is on the critical path | Its duration directly extends run-level wall-clock |
| The job is genuinely un-shardable | Cannot be split across multiple standard runners at lower cost |
| Δwall-clock is material (≥30% on that job) | A/B measured, not estimated |
| The run-level wall-clock actually drops | The job is not masked by a longer pole (e.g. Playwright ~22m) |
| The recurring $/min is accepted | Cost reviewed and approved |
If any criterion is false: reject. 2 free standard shards (4 effective cores, $0/min) strictly dominate 1 paid 4-core runner for parallelizable work.
Measurement procedure
- Take the baseline. Use the per-job wall-clock durations from
193-BASELINE.mdas your before-state. For the job under test, note: runner label, wall-clock, and cache-hit. - A/B test on a larger runner label (e.g.,
ubuntu-latest-4orubuntu-4-core). Run the job at least 3 times to average out scheduler jitter. - Measure: wall-clock, billed-minutes, and cache-hit for each run.
- Apply the decision rule from the table above. If a criterion fails, stop: reject and document the reason.
- Record a before/after table (template below) and include it in the phase SUMMARY.md.
Before/after table template
| Job | Runner label | Wall-clock | Billed-minutes | Cache-hit | Verdict |
|---|---|---|---|---|---|
| (job name) | ubuntu-latest | (baseline) | 0 (free) | (yes/no) | — baseline |
| (job name) | (larger label) | (A/B result) | (billed-mins) | (yes/no) | accept/reject |
Fill this table with real measurements before merging any larger-runner adoption. A missing or estimated table is grounds to revert the change in the next phase.
Current status
As of Phase 195, no larger runners are adopted. Every job in ci.yml uses runs-on: ubuntu-latest. The partition win (TEST-01: 2 standard shards halving library_tests walltime at $0/min) and the dep-off subset win (TEST-02: library_tests_dep_off targeting only the 65-test guard subset instead of the full suite) are the load-bearing Phase 195 gains. Larger runners were evaluated against the decision rule and rejected: the wall-clock pole is example_playwright_smoke (~22m, Phase 197 target), not any job that a larger runner would materially shorten.
Running the demo app
One command does everything:
scripts/uat/up.sh
That single command:
- starts a project-scoped Postgres and creates/migrates/seeds the
example_devdatabase, - builds + boots the demo as a container behind the shared Traefik proxy (bringing the proxy up automatically if it isn't running), with the repo bind-mounted for live reload,
- waits until the app actually responds (a readiness probe — the URL is never printed as live while it's still
STARTING), - auto-opens
http://…localhost/demo/credentials, and - prints grouped auth / admin / ops routes to copy-paste (sign in as
admin@demo.vaultr.testfor the admin routes).
Save a test/example template or a lib/ source file and it hot-reloads in the running container — no image rebuild. There's no second terminal: the server is already up by the time the script returns.
Flags
| Flag | What it does |
|---|---|
(none) / --proxy | The default: Dockerized demo behind shared Traefik, live reload, health-gated, auto-open. --proxy is just an explicit alias of the default. |
--dev / --host | Host-run Phoenix instead of the container — fastest live reload, no Docker app build. Starts + health-gates the server for you, in the background. |
--attach / --iex | Host-run in the foreground, bound to an IEx shell (Ctrl-C twice to stop). Implies the host-run path. |
--no-watch | Proxy mode without the bind-mount live-reload override (apply source changes with --refresh-code). |
--no-open | Don't auto-open the browser when the app is ready. |
--reset | Drop and recreate the demo database first. |
--no-seed | Skip seeding the demo personas. |
--private-traefik | Host-run fallback behind a project-private Traefik on :18080 (no shared :80). |
Running several Sigra-family libs at once
The shared proxy is what keeps multiple Elixir apps (each with its own admin UI) out of each other's way:
- One global Traefik (
scripts/dev-proxy/up.sh, auto-started) owns127.0.0.1:80and routes every project's.localhosthostname over the sharedproxyDocker network — so Sigra and sibling libraries coexist without anyone reserving port 80 or 4000. - Each checkout/branch gets a unique host (
sigra-<branch>-<hash>.localhost), derived so two branches or worktrees never collide. (Two Traefik routers with the same host would silently round-robin between them — the unique-by-construction host avoids that.) http://sigra.localhostis claim-based. The first stack to claim it — on any branch, not just the default — gets the friendly alias; everyone else keeps their unique per-checkout host. So you still get the clean URL on a feature branch.- Per-host cookie jars. A session on
sigra.localhostis not shared withsigra-feature-x.localhost. That's a feature for auth testing, not a bug. - Firefox / Safari /
curldon't resolve*.localhost. Use the printed RAW FALLBACKhttp://127.0.0.1:<port>for those.
How the Docker build caches your changes
The --proxy image (scripts/uat/Dockerfile.example) is layered so a change only rebuilds what it must — you will never re-download or recompile dependencies for a template tweak:
| You changed… | What rebuilds |
|---|---|
An example template / .heex / controller | Only the final example-compile layer (fast incremental). Deps + sigra stay cached. |
Sigra library source (lib/, priv/) | The dependency layer recompiles (sigra + its deps); the example recompiles. |
mix.exs / mix.lock | Dependencies re-resolve and recompile. |
After editing Sigra source while a --proxy stack is running, apply it with:
scripts/uat/up.sh --refresh-code
That does a cache-aware docker compose build + restart — not a from-scratch rebuild.
Iterating on code
- Day-to-day (default):
scripts/uat/up.sh. Save atest/exampletemplate orlib/source → it hot-reloads in the container (bind-mount + inotify). No image rebuild in the loop, and no second terminal. - Fastest reload:
scripts/uat/up.sh --devruns Phoenix on the host directly — no container layer at all. Tailtmp/uat-phoenix.logfor output, or--attachto drop into IEx. - No bind-mount:
--no-watchruns the container without the live-reload mount; pick up source changes withscripts/uat/up.sh --refresh-code(cache-aware rebuild + restart).
Teardown
scripts/db/down.sh # stop the test Postgres (--purge to also drop volumes)
scripts/uat/down.sh # stop the demo stack (--purge to drop the seeded demo DB)
Troubleshooting
FATAL: sorry, too many clients already— a shared/system Postgres is saturated. The Dockerized test DB (scripts/db/up.sh) is isolated and raised to 200 connections; make suretmp/db.envis loaded so tests use it, not a system5432.mix testconnects to the wrong DB — confirmtmp/db.envis loaded (direnv allow, orsource tmp/db.env). Unloaded, it defaults tolocalhost:5432by design.http://sigra.localhostdoesn't resolve — you're likely in Firefox/Safari/curl; use the printed RAW FALLBACK127.0.0.1:<port>.- Port 80 already owned — a sibling project's proxy already holds it; that's fine, Sigra routes through it. To run your own, set
SIGRA_DEV_PROXY_HTTP_PORT. - Demo URL says
STARTING — not yet responding— the readiness probe hasn't seen a 200 yet. Give it a few seconds; if it persists, check the logs. Host-run (--dev) logs are intmp/uat-phoenix.log; container logs aredocker compose … logs -f web(the exact command is printed under COMMANDS). - Hot reload feels slow on macOS — Docker bind-mount file-watching has noticeable latency on macOS. For the tightest save→reload loop, use
scripts/uat/up.sh --dev(host-run, no bind mount). - Browser looks stale after a source change with
--no-watch— you opted out of the live-reload mount; runscripts/uat/up.sh --refresh-code. different value ... for ExampleWeb.Endpoint— the image was built with different bind/port than runtime; rebuild with--refresh-code.
Where to go next
Demo showcase for the guided evaluator walkthrough · Testing auth flows for writing tests · Deployment for the production release image.