This is a personal proof-of-concept project. It is not intended for production use. Please refrain from using it.
Skip to content

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 … | bash pulls 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:

bash
curl -fsSL https://get.upliftr.io | bash
  • On your machine it comes up at https://upliftr.test behind a trusted local CA, green padlock, no warnings. (It maps upliftr.test in 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):

    bash
    UPLIFTR_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:

bash
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 --build

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

ServiceWhat it isPort
webThe React dashboard (static SPA) served by nginx, which reverse-proxies /api → the api.3400 (localhost)
apiThe FastAPI backend + execution engine (drives headless Chromium).8400 (localhost)
postgresBundled database (suites, runs, issues, plans, settings, users).internal
migrateApplies Alembic migrations, then exits. The api waits for it.,
docsThe VitePress docs site (nginx). Cloud overlay only.internal
caddyTLS 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

bash
git clone https://github.com/upliftrhq/upliftr.git /opt/upliftr
cd /opt/upliftr

2. Configure secrets

bash
cp .env.example .env
ini
# .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:

bash
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

Optionally pre-seed the first admin (otherwise you create it in the browser):

ini
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

bash
docker compose up -d --build

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

bash
docker compose ps          # services "running"/"healthy", migrate "exited (0)"
docker compose logs -f api

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

ini
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.yml

Then bring it up and point DNS (A records for both hosts) at the server:

bash
docker compose up -d --build

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

bash
# crontab -e, nightly at 02:00
0 2 * * * cd /opt/upliftr && ./deploy/backup.sh

Restore a dump:

bash
gunzip -c backups/upliftr_<timestamp>.sql.gz \
  | docker compose exec -T postgres psql -U upliftr upliftr

Keep UPLIFTR_SECRET_KEY alongside the dumps, the encrypted settings inside are useless without it.

Upgrades

bash
git pull
./deploy/upgrade.sh

It 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 (default 2) 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) and UPLIFTR_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:

ini
UPLIFTR_DATABASE_URL=postgresql+psycopg://user:pass@db.internal:5432/upliftr
bash
docker 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

bash
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 strong POSTGRES_PASSWORD, UPLIFTR_JWT_SECRET, and UPLIFTR_SECRET_KEY set 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/ postgres not publicly exposed.
  • [ ] DNS A records for app. (and docs.) point at the host.
  • [ ] First admin created; team invited from Settings.
  • [ ] Nightly deploy/backup.sh; UPLIFTR_SECRET_KEY stored with the backups.
  • [ ] UPLIFTR_MAX_CONCURRENT_RUNS and cost caps sized to the host/budget.
  • [ ] Integration credentials use a dedicated service account, not a personal token.

Troubleshooting

SymptomLikely cause / fix
api stuck "starting", never healthyIt 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 upDNS 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 401Expected 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 moveUPLIFTR_SECRET_KEY changed or wasn't set, encrypted values can't be decrypted. Restore the original key.
Screenshots/artifacts 404 in the dashboardThe 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 errorCheck 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.

Upliftr · AI-native, self-healing E2E testing. Terms · Privacy · Legal Notice