Self-hosting Upliftr
Run Upliftr's AI debugging agents on your own infrastructure with Docker, the API + engine, the dashboard, the docs, and a Postgres database, behind an automatic-HTTPS front door, with migrations, health checks, backups, and zero-downtime upgrades. Self-hosted and bring-your-own-Anthropic-key: the agents are read-only by design, they drive a real browser, read your servers and logs to root-cause failures to the exact backend line, and re-run the checks they save, all in your own environment on hardware you control. They investigate and diagnose; they never mutate your code, your data, or your infrastructure.
There are three paths:
- One line,
curl … | bashpulls prebuilt images. Easiest; great for a laptop or a simple server. - From source, builds the images and fronts your own domains with automatic Let's Encrypt. Best for a public production server.
- Manual setup, wire each piece by hand (custom proxy, external Postgres, …).
One line (prebuilt images)
The fastest way to self-host. One command pulls the prebuilt images (no source, no build), generates every secret, brings up the stack (API + dashboard + Postgres), creates your admin, and prints the login:
curl -fsSL https://get.upliftr.io | bashOn your machine it comes up at
https://upliftr.testbehind a trusted local CA, green padlock, no warnings. (It mapsupliftr.testin your hosts file and installs the CA.)On a server, set a domain and it serves there over plain http, front it with your own TLS (or use the from-source path for automatic certs):
bashUPLIFTR_DOMAIN=app.yourco.com curl -fsSL https://get.upliftr.io | bash
Everything lands in ~/upliftr (or /opt/upliftr as root). Re-run the same command to upgrade, it reuses your .env, pulls the latest images, and keeps your data (the database lives in a named volume that an upgrade never touches). The installer pre-flights Docker, Compose, and the ports, and reclaims any stale Upliftr containers before it starts.
Optional knobs (env vars before the pipe)
ANTHROPIC_API_KEY=sk-… seed the engine key · UPLIFTR_DOMAIN=upliftr.acme.com install on a server · UPLIFTR_DIR=/opt/upliftr install location.
Save the admin password
It's printed once. Store it in your password manager and change it after first login in Settings.
From source (build it yourself)
Prefer to build the images and run the bundled Caddy for automatic Let's Encrypt on your own domains? Clone the repo, generate secrets, set your hostnames, and bring it up, Caddy provisions certificates and routes app. → the dashboard and docs. → the docs:
git clone https://github.com/upliftrhq/upliftr.git /opt/upliftr
cd /opt/upliftr
cp .env.example .env
# Generate strong secrets:
echo "POSTGRES_PASSWORD=$(openssl rand -hex 24)" >> .env
echo "UPLIFTR_JWT_SECRET=$(openssl rand -hex 48)" >> .env
echo "UPLIFTR_SECRET_KEY=$(openssl rand -hex 48)" >> .env
# Your domains + the TLS/docs overlay (compose reads COMPOSE_FILE natively):
cat >> .env <<'EOF'
UPLIFTR_DEPLOYMENT_MODE=selfhosted
UPLIFTR_APP_HOST=app.yourco.com
UPLIFTR_DOCS_HOST=docs.yourco.com
COMPOSE_FILE=docker-compose.yml:docker-compose.cloud.yml
EOF
docker compose up -d --buildOn first boot, in order: postgres becomes healthy → migrate applies the schema → api passes its health check → web and docs start behind Caddy. Point your domains' A records at the host:
app.yourco.com → <this host's IP>
docs.yourco.com → <this host's IP>HTTPS comes up automatically within a minute. On first load you'll get a one-time "Create your admin" screen (or pre-seed UPLIFTR_ADMIN_EMAIL / UPLIFTR_ADMIN_PASSWORD in .env).
Re-running is safe
docker compose up -d --build is idempotent, run it again any time to pick up changes; it reuses your .env and keeps your data. For upgrades use git pull && ./deploy/upgrade.sh (see Day-2 operations).
See Manual setup below for the full breakdown, every variable, an external Postgres, or bringing your own proxy instead of Caddy.
Manual setup
Prefer to wire everything yourself, a custom reverse proxy, an external Postgres, or no docs site? Here's every step the installer automates.
What you'll run
docker compose up brings up the core services; the cloud overlay adds the docs site and a Caddy TLS front door:
| Service | What it is | Port |
|---|---|---|
| web | The React dashboard (static SPA) served by nginx, which reverse-proxies /api → the api. | 3400 (localhost) |
| api | The FastAPI backend + execution engine (drives headless Chromium). | 8400 (localhost) |
| postgres | Bundled database (suites, runs, issues, plans, settings, users). | internal |
| migrate | Applies Alembic migrations, then exits. The api waits for it. | , |
| docs | The VitePress docs site (nginx). Cloud overlay only. | internal |
| caddy | TLS front door, terminates HTTPS and routes app. → web, docs. → docs. Cloud overlay only. | 80 / 443 |
The dashboard calls the API over a relative /api path, so the browser only talks to the front door; web, api, and postgres are bound to localhost / the internal Docker network and are never exposed directly.
Prerequisites
- A Linux host (a 2 vCPU / 4 GB VM is a reasonable start, each concurrent run drives a real Chromium, so size up if you raise
UPLIFTR_MAX_CONCURRENT_RUNS). - Docker Engine 24+ and Docker Compose v2 (
docker compose version). - An Anthropic API key (
sk-ant-…), Claude is the engine that drives the browser. (Optional up front; orgs can BYO key in the dashboard.) - A domain + DNS if exposing beyond localhost.
1. Get the code
git clone https://github.com/upliftrhq/upliftr.git /opt/upliftr
cd /opt/upliftr2. Configure secrets
cp .env.example .env# .env, the minimum for a real deployment
UPLIFTR_DEPLOYMENT_MODE=selfhosted
ANTHROPIC_API_KEY=sk-ant-...
# Database, compose builds the api's connection string from these.
POSTGRES_PASSWORD=<a strong random password>
# Set these so sessions and encrypted secrets survive restarts/migrations.
UPLIFTR_JWT_SECRET=<random>
UPLIFTR_SECRET_KEY=<random>Keep UPLIFTR_SECRET_KEY safe
It encrypts integration credentials at rest. If you lose it you can't decrypt stored secrets, and a database restored onto a new host needs the same key. Store it in your secret manager and back it up with your database dumps.
Generate strong values quickly:
echo "POSTGRES_PASSWORD=$(openssl rand -hex 24)" >> .env
echo "UPLIFTR_JWT_SECRET=$(openssl rand -hex 48)" >> .env
echo "UPLIFTR_SECRET_KEY=$(openssl rand -hex 48)" >> .envOptionally pre-seed the first admin (otherwise you create it in the browser):
UPLIFTR_ADMIN_EMAIL=admin@yourco.com
UPLIFTR_ADMIN_PASSWORD=<at least 8 chars>See the full Settings & env reference for every variable.
3. Bring it up
docker compose up -d --buildOn first boot, in order: postgres becomes healthy → migrate applies the schema and exits → api starts and passes its health check (/api/v1/health/ready) → web starts. Watch it settle:
docker compose ps # services "running"/"healthy", migrate "exited (0)"
docker compose logs -f apiContainers run as non-root; data persists in two named volumes (upliftr-pgdata for the database, upliftr-data for run-artifact screenshots).
4. Create the first admin
If you didn't pre-seed an admin, browse to the dashboard and you'll get a one-time "Create your admin account" screen. Set an email + password (min 8 chars) and you're in. From Settings you can invite the rest of your team. See Authentication.
5. Put TLS in front
The simplest path is the bundled Caddy front door, the same one the installer uses. Set the hostnames and select the overlay in .env:
UPLIFTR_APP_HOST=upliftr.yourco.com
UPLIFTR_DOCS_HOST=docs.yourco.com
# Make every `docker compose` command use the TLS + docs overlay:
COMPOSE_FILE=docker-compose.yml:docker-compose.cloud.ymlThen bring it up and point DNS (A records for both hosts) at the server:
docker compose up -d --buildCaddy auto-provisions Let's Encrypt certificates and routes app. → the dashboard and docs. → the docs. No Caddyfile editing, the hostnames come from .env.
Bring your own proxy
Prefer Traefik / nginx / an existing ingress? Skip the overlay (leave COMPOSE_FILE unset), keep web and api on localhost, and point your proxy at localhost:3400 (the dashboard, which proxies /api itself). Set UPLIFTR_CORS_ORIGINS=["https://upliftr.yourco.com"] and recreate the api.
Day-2 operations
Backups
./deploy/backup.sh dumps the database (pg_dump | gzip) to ./backups/ and keeps the 14 most recent. Schedule it:
# crontab -e, nightly at 02:00
0 2 * * * cd /opt/upliftr && ./deploy/backup.shRestore a dump:
gunzip -c backups/upliftr_<timestamp>.sql.gz \
| docker compose exec -T postgres psql -U upliftr upliftrKeep UPLIFTR_SECRET_KEY alongside the dumps, the encrypted settings inside are useless without it.
Upgrades
git pull
./deploy/upgrade.shIt backs up → builds → runs migrations → rolls every app container (api, web, docs) → reloads Caddy → waits for readiness. It upgrades whatever services your COMPOSE_FILE selects, so the same command serves both the core and the TLS-front-door stacks. With the docker-rollout plugin (the installer adds it) it does a true zero-downtime swap, start new, wait healthy, stop old; without it, it recreates the services (a brief blip). The api drains in-flight requests on shutdown, and the durable run queue reconciles any run interrupted by the restart.
Scaling & cost control
- Concurrency,
UPLIFTR_MAX_CONCURRENT_RUNS(default2) sets how many runs execute in parallel; each is a browser + LLM calls, so size it to the host. Upliftr runs as a single process, scale runs with this knob, not by running multiple api replicas. - Cost caps,
UPLIFTR_RUN_COST_CAP_USD(per run) andUPLIFTR_DAILY_COST_CAP_USD(rolling 24h) bound model spend; an aborted run reports the reason. - Login abuse, failed logins are rate-limited per IP (10 / 5 min →
429); add proxy-level limits too on a public deployment.
Using a managed/external Postgres
Point the api at it instead of the bundled service, then run migrations against it:
UPLIFTR_DATABASE_URL=postgresql+psycopg://user:pass@db.internal:5432/upliftrdocker compose run --rm migrate # applies alembic upgrade head to your DB
docker compose up -d api web # (you can drop the bundled postgres service)Tune the pool with UPLIFTR_DB_POOL_SIZE / UPLIFTR_DB_MAX_OVERFLOW / UPLIFTR_DB_POOL_TIMEOUT.
Uninstall
docker compose down # stop the stack, keep your data
docker compose down -v # stop AND delete the data volumes (irreversible)Production checklist
- [ ]
ANTHROPIC_API_KEY(or BYO per-org), a strongPOSTGRES_PASSWORD,UPLIFTR_JWT_SECRET, andUPLIFTR_SECRET_KEYset in.env(secrets in a manager, not in git). - [ ] TLS front door up (
UPLIFTR_APP_HOST+ the cloud overlay, or your own proxy);web/api/postgresnot publicly exposed. - [ ] DNS
Arecords forapp.(anddocs.) point at the host. - [ ] First admin created; team invited from Settings.
- [ ] Nightly
deploy/backup.sh;UPLIFTR_SECRET_KEYstored with the backups. - [ ]
UPLIFTR_MAX_CONCURRENT_RUNSand cost caps sized to the host/budget. - [ ] Integration credentials use a dedicated service account, not a personal token.
Troubleshooting
| Symptom | Likely cause / fix |
|---|---|
api stuck "starting", never healthy | It can't reach Postgres or migrations failed. docker compose logs migrate api; confirm POSTGRES_PASSWORD matches what the db came up with (a changed password on an existing upliftr-pgdata volume won't apply, reset the volume or set the old password). |
| HTTPS doesn't come up | DNS for UPLIFTR_APP_HOST / UPLIFTR_DOCS_HOST must resolve to this host and ports 80/443 must be open, Caddy needs both to complete the ACME challenge. docker compose logs caddy. |
| Dashboard loads but every call is 401 | Expected until you create/sign in as the admin. If sessions drop after a restart, set UPLIFTR_JWT_SECRET (otherwise it's regenerated each boot). |
| Stored integration secrets "disappear" after a move | UPLIFTR_SECRET_KEY changed or wasn't set, encrypted values can't be decrypted. Restore the original key. |
| Screenshots/artifacts 404 in the dashboard | The upliftr-data volume isn't persisted, or a proxy is stripping the ?token= query param on /api/v1/artifacts. |
| Runs fail instantly with a model error | Check ANTHROPIC_API_KEY, and any UPLIFTR_RUN_COST_CAP_USD/DAILY_COST_CAP_USD that may be aborting them. |
Next: the Deployment reference for the compose internals, and Settings & env for every variable.