Deployment¶
Run from source¶
To run from source, start only the database and point verifair at it:
docker compose up -d db
uv sync
export VERIFAIR_API_TOKEN=change-me
export VERIFAIR_DATABASE_URL=postgresql://verifair:verifair@localhost:5432/verifair
uv run uvicorn verifair.asgi:app
Every start runs the schema SQL, which is safe to repeat. It runs under an advisory lock, so several copies can start at once. CREATE INDEX IF NOT EXISTS takes a lock that waits behind writes in progress, so a start can wait up to 5 seconds. After that it stops with Startup waited too long for a database lock held by another transaction. Restart to try again.
Plan-first reviews add a fixed workflow and target-specific review records, allow a snapshot without an implementation, and backfill the revision associated with each saved plan. Existing reports remain artifact reviews; existing comments, signatures and verdicts remain implementation records. The migration preserves saved Markdown and is safe to repeat under the same startup lock. Back up first, stop older application instances, and restart all instances on this version before accepting plan-first submissions. Do not mix older readers or writers with migrated data: they cannot inherit plans correctly and assume every snapshot has an implementation body. Downgrading is not supported after plan-first records exist; restore the backup into a separate empty database if a rollback is needed. Keep the migrated database for its review history.
Older versions kept their data in a SQLite file, verifair.db. This version does not read that file or move its data. Start with an empty database.
Docker¶
CI publishes the image to ghcr.io/romanzdk/verifair for linux/amd64 and linux/arm64. It is public, so pulling needs no login.
| Tag | Published on |
|---|---|
main, sha-<short commit> (for example sha-1787484) |
every push to main |
1.2.3, latest, sha-<short commit> |
a v1.2.3 git tag on a commit in main |
Other git tags, like v1.2 or v1.2.3-rc.1, fail the publish job and publish nothing.
Deploy a version tag or sha-<short commit>, not main or latest, so a restart does not pull a different version. To build it yourself, run docker build -t verifair ..
docker run --rm -e VERIFAIR_API_TOKEN=change-me \
-e VERIFAIR_DATABASE_URL=postgresql://verifair:verifair@host.docker.internal:5432/verifair \
-p 127.0.0.1:8000:8000 ghcr.io/romanzdk/verifair:1.2.3
The image runs as user 10001, not root, so it works with Kubernetes runAsNonRoot: true. With readOnlyRootFilesystem: true, mount an emptyDir at /tmp, because uv run writes there at start.
This needs a running Postgres. host.docker.internal reaches a Postgres on your own machine (Docker Desktop). On Linux, or for another server, replace it with the database host.
The image listens on all addresses (UVICORN_HOST=0.0.0.0). When a TLS proxy runs next to verifair in the same pod, you can set UVICORN_HOST=127.0.0.1 so only the proxy can reach it. The image HEALTHCHECK still works then, because it runs inside the container.
Deploying¶
- In local mode the web UI has no login. Anyone who can open it can comment and give verdicts. Run it on a private network or behind an auth proxy (SSO, basic auth). For a team, use shared mode, where verifair signs people in itself with your OpenID Connect provider (see access.md). The Docker example above only listens on
127.0.0.1. - HTTPS is required for browser access outside loopback, including private-network hostnames. Local development may use HTTP on
localhost,127.0.0.1,[::1]or0.0.0.0. In local mode, on these addresses (any127.x.x.xtoo), a browser post withoutSec-Fetch-Siteis accepted when itsOriginmatches the address, because some browsers leave the header out onhttp://0.0.0.0. Put a TLS reverse proxy in front of the loopback listener for shared deployments. Ordinary HTTP hostnames can load pages but browser writes return 403 because browsers omit the requiredSec-Fetch-Siteheader on those origins. - To sign people in, set
VERIFAIR_OIDC_ISSUER,VERIFAIR_OIDC_CLIENT_ID,VERIFAIR_OIDC_CLIENT_SECRETandVERIFAIR_SESSION_SECRET(see access.md). This turns on shared mode. The email from sign-in becomes the user id; the display name comes from the member list. The name prompt goes away, and signing and approvals are tied to that identity. No auth proxy is needed. - With several replicas, give every replica the same
VERIFAIR_SESSION_SECRET. A replica with another secret does not accept the session cookie and sends people to sign in again. - verifair must be able to reach the provider over HTTPS (for Google:
accounts.google.comandoauth2.googleapis.com) for sign-in. Already signed-in people keep working while the provider is down. - Set
VERIFAIR_BASE_URLto the public HTTPS URL, for examplehttps://verifair.example.com. It is used in notification links, in the push response and as the sign-in redirect URI (VERIFAIR_BASE_URL/auth/callback), and enables secure cookies. Shared mode refuses to start with an HTTP base URL other thanlocalhost. Keep the browser on this origin and have the reverse proxy preserveSec-Fetch-Site. - Serve it at the root of a domain. Sub-paths (like
/verifair/) are not supported. - verifair needs a Postgres 16 or newer database. Its database role must own the schema and the tables, because every start runs the schema SQL, and Postgres 15 and newer does not let every role create tables in
public. Where Postgres runs in production is not decided here. - For a database on another host, add
sslmode=verify-full(or at leastsslmode=require) toVERIFAIR_DATABASE_URL, for examplepostgresql://verifair:secret@db.example.com:5432/verifair?sslmode=verify-full. - Behind PgBouncer, use session mode. Transaction mode needs PgBouncer 1.21 or newer with
max_prepared_statementsset, because psycopg makes prepared statements and they cannot be turned off from the URL. - Startup creates and updates the schema under an advisory lock, so several replicas can start at once. A start can wait up to 5 seconds behind writes in progress, then stops with a clear message. Restart it to try again.
- You can run several replicas, because all state is in Postgres. Keep
(replicas + extra pods during a rolling update) × VERIFAIR_DB_POOL_SIZE, plus the connections you need for admin work and backups, below the server'smax_connections. - Back up with
pg_dump -Fc "$VERIFAIR_DATABASE_URL" > verifair.dump. Restore into an empty database as verifair's role withpg_restore --no-owner -d <empty database> verifair.dump, so that role owns the restored tables. - The image trusts
X-Forwarded-*headers from any address. SetFORWARDED_ALLOW_IPSto your proxy's IP. - Use
/healthzfor the liveness probe and/readyzfor the readiness probe./healthzdoes not touch the database, so a database outage does not restart pods./readyzreturns 200{"ok": true}when the database answersSELECT 1, and 503{"ok": false}withRetry-After: 5when it is down or no connection is free within 2 seconds, so the pod stops getting traffic until the database is back. Both are public, also in shared mode. - Give both probes a timeout of at least 5 seconds (
timeoutSeconds: 5). Under heavy load the answer can take a few seconds, and a shorter timeout restarts a pod that is only busy. - The probes need no sign-in and no proxy rules. With
UVICORN_HOST=127.0.0.1, an HTTP probe from outside the container cannot reach/healthz; use an exec probe with the same command as the imageHEALTHCHECKthen.
Upgrading an existing HTTP deployment¶
Before upgrading a shared deployment, configure a trusted TLS certificate and an HTTPS reverse proxy for its hostname. Redirect the old HTTP URL to HTTPS, set VERIFAIR_BASE_URL to that HTTPS origin, and restart verifair behind the proxy with its existing database. Update agent URLs and bookmarked report links to HTTPS. A healthy /healthz response or a readable page on HTTP does not confirm that browser writes work.
After restarting, open the HTTPS URL in a browser. In local mode, set your name if needed; in shared mode, sign in as a member first. Then submit a comment on a draft report to check that writes pass through the proxy. Existing non-loopback HTTP deployments need this change before the upgrade; private-network placement alone does not enable browser writes. The API continues to use bearer tokens, and /healthz can still be checked over the internal loopback HTTP listener.