Deploying Upliftr
Upliftr is AI debugging agents that debug your app: you describe a bug or a flow in plain English, an agent drives a real browser to reproduce it, root-causes the failure to the exact backend line, and files a deduped ticket. The agents are non-destructive by design, read-only, they investigate and diagnose, they never mutate your code, data, or infrastructure. You self-host them, bringing your own Anthropic key. It ships as two apps, a FastAPI backend (API + engine) and a Vite/React frontend (dashboard). The default execution path is cross-platform Playwright, so runs work on Linux, Windows, and macOS. This page covers Docker Compose, self-hosting, and CI runners.
docker-compose
The repo ships a docker-compose.yml that brings up the API + dashboard surface:
cp .env.example .env # add ANTHROPIC_API_KEY (+ tracker creds if wanted)
docker compose up --build- API + OpenAPI docs →
http://localhost:8400/docs - Dashboard →
http://localhost:3400
Compose brings up four things: a bundled Postgres (no manual install), a migrate service that applies the schema (alembic upgrade head) and exits, the api (which waits for Postgres to be healthy and migrations to finish), and the web dashboard (which waits for the api to be healthy). Containers run as non-root.
The API is authenticated, the first time you open the dashboard it shows a "Create your admin account" screen (or pre-seed the admin with UPLIFTR_ADMIN_EMAIL + UPLIFTR_ADMIN_PASSWORD for headless/CI). See Authentication.
services:
postgres:
image: postgres:17-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER:-upliftr}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-upliftr} # set a strong one in prod
POSTGRES_DB: ${POSTGRES_DB:-upliftr}
volumes: [upliftr-pgdata:/var/lib/postgresql/data]
healthcheck: # api waits on this
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-upliftr}"]
migrate: # runs alembic, then exits
image: ghcr.io/upliftrhq/upliftr-api
command: ["alembic", "upgrade", "head"]
depends_on: { postgres: { condition: service_healthy } }
api:
build: ./backend
ports: ["8400:8400"]
environment:
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
UPLIFTR_DATA_DIR: /data # run artifacts (named volume)
UPLIFTR_DATABASE_URL: postgresql+psycopg://upliftr:upliftr@postgres:5432/upliftr
UPLIFTR_JWT_SECRET: ${UPLIFTR_JWT_SECRET:-} # set in prod
UPLIFTR_SECRET_KEY: ${UPLIFTR_SECRET_KEY:-} # set in prod
volumes: [upliftr-data:/data]
depends_on:
postgres: { condition: service_healthy }
migrate: { condition: service_completed_successfully }
healthcheck: # GET /health/ready (db + disk)
test: ["CMD", "python", "-c", "import httpx,sys; sys.exit(0 if httpx.get('http://localhost:8400/api/v1/health/ready').status_code==200 else 1)"]
stop_grace_period: 25s # drain in-flight on SIGTERM
web:
build: ./frontend
ports: ["3400:3400"]
depends_on: { api: { condition: service_healthy } }Pointing the dashboard at a remote API
The dashboard calls a relative /api path; the bundled nginx proxies it to the api service. If you serve the dashboard separately from the API, set VITE_API_BASE at build time (it's baked into the bundle, not read at runtime) to the API's public URL.
Persist the two named volumes, upliftr-pgdata (the Postgres database: suites, run history, the built-in issue tracker, plans, settings, users) and upliftr-data (run artifacts / screenshots, under UPLIFTR_DATA_DIR). Provide credentials (the Anthropic key, and any GitHub / GitLab / Jira tokens, and a log backend) via environment or the Settings API. See Settings & env.
Compose covers the API + UI surface
The compose containers run the API and dashboard. The default Playwright runner drives a headless Chromium and is cross-platform, to run actual browser tests inside a container, base the API image on a Playwright-ready image (or install playwright install --with-deps chromium) so Chromium and its system libraries are present.
Database, Postgres in Docker, SQLite for quick local dev
Structured data lives in a SQL database via SQLAlchemy, with a deliberate split so prod is robust and local stays instant:
| Where | Database | Schema |
|---|---|---|
docker compose / production | Postgres (bundled, no manual install) | Owned by Alembic, applied by the migrate service |
Host-native make dev-api | SQLite file under UPLIFTR_DATA_DIR (zero setup) | Auto-created from the models |
| Test suite | throwaway SQLite per test | Auto-created |
Compose builds the API's UPLIFTR_DATABASE_URL from POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB automatically, set a strong POSTGRES_PASSWORD in .env for production. To use an external/managed Postgres, set UPLIFTR_DATABASE_URL to override the bundled one.
Migrations. Postgres schema changes are versioned with Alembic and applied by the migrate service on every up (or run by hand with make migrate / alembic upgrade head). SQLite dev/test auto-creates the schema from the models, so there's nothing to run locally.
Parity. CI runs the full backend test suite against both SQLite and Postgres, plus a job that asserts alembic upgrade head applies cleanly, so "works locally, breaks in prod" can't slip through. Locally you can do the same with make dev-db && make test-backend-pg.
Want host-native dev on Postgres too?
make dev-db starts just the Postgres container; then export UPLIFTR_DATABASE_URL=postgresql+psycopg://upliftr:upliftr@localhost:5432/upliftr, make migrate, and make dev-api.
Zero-downtime upgrades
./deploy/upgrade.sh performs a safe rolling upgrade: back up → build → migrate → roll the api and web containers → wait for readiness. With the optional docker-rollout plugin installed it does a true zero-downtime swap (start the new container, wait for its healthcheck, then stop the old one); without it, it recreates the services. The api ships a --timeout-graceful-shutdown and a stop_grace_period, so in-flight requests drain instead of being cut off, and the healthcheck-gated depends_on means the dashboard never points at an api that isn't ready.
Production checklist: auth, secrets, scaling
- Auth, set
UPLIFTR_ADMIN_EMAIL/UPLIFTR_ADMIN_PASSWORDfor the first admin, andUPLIFTR_JWT_SECRETso sessions survive restarts. CI and the MCP server authenticate with API tokens. See Authentication. - Secrets at rest, UI-set credentials are encrypted in the database with
UPLIFTR_SECRET_KEY. Set it (and back it up) so encrypted secrets survive a DB move or restore. Process env vars always take precedence over stored settings. - Concurrency,
UPLIFTR_MAX_CONCURRENT_RUNS(default2) sets how many runs execute in parallel; each is a browser plus LLM calls, so size it to the host. The queue is durable , runs persist as rows and a server restart reconciles anything left mid-run. - Cost caps, bound model spend with
UPLIFTR_RUN_COST_CAP_USD(per run) andUPLIFTR_DAILY_COST_CAP_USD(rolling 24h); an aborted run reports the reason. Per-requestUPLIFTR_LLM_TIMEOUTandUPLIFTR_LLM_RETRIESharden against transient model errors. - Abuse, failed logins are rate-limited per IP (10 / 5 min →
429). On a public deployment, also rate-limit at your reverse proxy and terminate TLS there.
Backup, restore & key rotation
Upliftr's state is two things: the database (suites, runs, issues, plans, settings, users, API tokens, integration secrets encrypted) and the artifacts directory (UPLIFTR_DATA_DIR, the run screenshots). Back up both.
- What to keep safe,
UPLIFTR_SECRET_KEY(decrypts stored secrets) andUPLIFTR_JWT_SECRET(signs sessions). Store them in your secret manager. If you loseUPLIFTR_SECRET_KEY, the encrypted settings in the DB can't be decrypted, you'd re-enter every integration credential. A restored DB on a new host needs the sameUPLIFTR_SECRET_KEY. - Postgres (compose), run
./deploy/backup.sh: itpg_dump | gzips the database to./backups/and keeps the 14 most recent.upgrade.shcalls it automatically before every upgrade. Restore withgunzip -c backups/upliftr_<ts>.sql.gz | docker compose exec -T postgres psql -U upliftr upliftr. Schedule it (cron) and keep the secret key with the dumps. - SQLite (host-native dev), back up the
upliftr.dbfile underUPLIFTR_DATA_DIR(stop the API or copy the WAL alongside it), plus theartifacts/subtree. - Rotating
UPLIFTR_SECRET_KEY, re-encryption isn't automatic. To rotate: with the old key still set, re-save each secret setting through the dashboard/API so it re-encrypts under the new key, or clear and re-enter credentials after switching keys. Don't just swap the key on a populated DB, existing ciphertext won't decrypt.
Self-hosting
Put a reverse proxy / TLS terminator in front of both services:
- API, containerised from
backend/Dockerfile(compose serviceapi, port 8400). ProvideANTHROPIC_API_KEYand any tracker / log credentials via env or a secret manager; mount a volume atUPLIFTR_DATA_DIRto persist the SQLite DB + artifacts (or use Postgres, above). SetUPLIFTR_JWT_SECRETandUPLIFTR_SECRET_KEYso sessions and encrypted secrets survive restarts. - Dashboard, built from
frontend/Dockerfile(serviceweb, port 3400). PointVITE_API_BASEat the API's public URL. - CORS, include the dashboard's public origin in
UPLIFTR_CORS_ORIGINS. - Secrets, integration tokens and the Anthropic key are encrypted at rest and secret-masked on read; keep them in env / a secret manager, never in git.
- Network & trust, by design, an Upliftr agent drives a browser against whatever URL an authenticated user points it at, including internal /
localhosttargets (that's the job, debugging your own apps). So treat any authenticated user as able to reach the API host's network: keep the instance on a trusted network behind your proxy, and only invite users you'd trust with that reach. Run-ids are unguessable and artifacts are authenticated, so they aren't enumerable by an outsider.
CI runners
The default Playwright path runs fine on standard Linux CI runners, no macOS host required. The simplest integrations are the GitHub Action or GitLab CI, which sets up Python + uv, installs Chromium, and re-runs the checks the agent saved with a meaningful exit code. For other CI systems, run the CLI directly:
cd backend
uv venv
uv pip install "pyyaml>=6.0" "anthropic>=0.49" "httpx>=0.28" "pillow>=11.0" \
"websocket-client>=1.8" "playwright>=1.40"
source .venv/bin/activate
playwright install --with-deps chromium
ANTHROPIC_API_KEY=$KEY upliftr run "suites/*.upliftr.yaml" \
--file-issues github --out artifactsThe CLI exits 0 (pass) / 1 (failed) / 2 (broken) / 3 (config error), so the job gates the PR. Verified failures file deduped, root-caused issues to your configured destinations, a failed CI run leaves a triaged ticket behind, not just a red check.
Legacy macOS desktop driver
There's an optional legacy driver (UPLIFTR_DRIVER=desktop) that drives the local macOS browser via native Quartz events (mac_control.py), requiring a macOS host with a display session. It is not needed for normal deployment, the cross-platform playwright driver (the default) is the supported path everywhere. Only choose desktop if you specifically want to drive a real on-screen macOS browser.
Checklist
- [ ] API reachable on
/api/v1;GET /api/v1/healthreturns OK (and/health/readyis 200). - [ ]
ANTHROPIC_API_KEYset for the engine. - [ ] First admin set (
UPLIFTR_ADMIN_EMAIL/UPLIFTR_ADMIN_PASSWORD);UPLIFTR_JWT_SECRET+UPLIFTR_SECRET_KEYset for production. - [ ] Database chosen, SQLite volume persisted, or
UPLIFTR_DATABASE_URLpointed at Postgres. - [ ] Chromium available to the runner (
playwright install chromium). - [ ] Data volume persisted (
UPLIFTR_DATA_DIR) for artifacts (+ SQLite). - [ ]
UPLIFTR_CORS_ORIGINSincludes the dashboard's origin;VITE_API_BASEpoints at the API. - [ ] Concurrency (
UPLIFTR_MAX_CONCURRENT_RUNS) + cost caps sized for the host. - [ ] Issue destinations + a log backend configured in Settings.
Related: Getting Started · CI · Settings & env