Self-hosting Tripwire
Run the whole platform 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.
There are two paths:
- Quick install — one command does everything. Recommended.
- Manual setup — wire each piece by hand (custom proxy, external Postgres, …).
If you just want to run it on your laptop to develop or evaluate, see Local development instead.
Quick install (recommended)
On a fresh Linux host with Docker installed, one command stands up the entire stack — it generates every secret, writes .env, brings the platform up behind a TLS front door, creates your admin, and prints the login:
git clone https://github.com/tripwire-e2e/tripwire.git /opt/tripwire
cd /opt/tripwire
sudo ./install/install.shThe installer:
- Pre-flights the host — checks Docker, Compose, and ports 80/443, and installs the
docker-rolloutplugin so upgrades are zero-downtime. - Prompts for your dashboard domain, docs domain, admin email, and Anthropic key. (The key is the instance fallback — each org can also bring its own later in the dashboard, so you can leave it blank and set it in the UI.)
- Generates every secret —
TRIPWIRE_SECRET_KEY,TRIPWIRE_JWT_SECRET, the database password, and a strong admin password — into achmod 600.env. - Builds, migrates, and starts Postgres + api + web + docs behind Caddy, which auto-provisions Let's Encrypt certificates.
- Creates your admin (org + owner) on first boot and prints the one-time login plus the DNS records to add.
Then point your domain's 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. That's it — skip to Day-2 operations.
Re-running is safe
./install/install.sh is idempotent: run it again any time and it reuses your existing .env and just brings the stack up to date.
Save the admin password
It's printed once. Store it in your password manager and change it after first login in Settings. (Lose it before then? Re-create the admin by setting TRIPWIRE_ADMIN_EMAIL / TRIPWIRE_ADMIN_PASSWORD in .env against a fresh database.)
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
TRIPWIRE_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/tripwire-e2e/tripwire.git /opt/tripwire
cd /opt/tripwire2. Configure secrets
cp .env.example .env# .env — the minimum for a real deployment
TRIPWIRE_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.
TRIPWIRE_JWT_SECRET=<random>
TRIPWIRE_SECRET_KEY=<random>Keep TRIPWIRE_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 "TRIPWIRE_JWT_SECRET=$(openssl rand -hex 48)" >> .env
echo "TRIPWIRE_SECRET_KEY=$(openssl rand -hex 48)" >> .envOptionally pre-seed the first admin (otherwise you create it in the browser):
TRIPWIRE_ADMIN_EMAIL=admin@yourco.com
TRIPWIRE_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 (tripwire-pgdata for the database, tripwire-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:
TRIPWIRE_APP_HOST=tripwire.yourco.com
TRIPWIRE_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 TRIPWIRE_CORS_ORIGINS=["https://tripwire.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/tripwire && ./deploy/backup.shRestore a dump:
gunzip -c backups/tripwire_<timestamp>.sql.gz \
| docker compose exec -T postgres psql -U tripwire tripwireKeep TRIPWIRE_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 —
TRIPWIRE_MAX_CONCURRENT_RUNS(default2) sets how many runs execute in parallel; each is a browser + LLM calls, so size it to the host. Tripwire runs as a single process — scale runs with this knob, not by running multiple api replicas. - Cost caps —
TRIPWIRE_RUN_COST_CAP_USD(per run) andTRIPWIRE_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:
TRIPWIRE_DATABASE_URL=postgresql+psycopg://user:pass@db.internal:5432/tripwiredocker 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 TRIPWIRE_DB_POOL_SIZE / TRIPWIRE_DB_MAX_OVERFLOW / TRIPWIRE_DB_POOL_TIMEOUT.
Uninstall
./install/uninstall.sh # stops the stack; prompts before deleting data volumesProduction checklist
- [ ]
ANTHROPIC_API_KEY(or BYO per-org), a strongPOSTGRES_PASSWORD,TRIPWIRE_JWT_SECRET, andTRIPWIRE_SECRET_KEYset in.env(secrets in a manager, not in git). - [ ] TLS front door up (
TRIPWIRE_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;TRIPWIRE_SECRET_KEYstored with the backups. - [ ]
TRIPWIRE_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 tripwire-pgdata volume won't apply — reset the volume or set the old password). |
| HTTPS doesn't come up | DNS for TRIPWIRE_APP_HOST / TRIPWIRE_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 TRIPWIRE_JWT_SECRET (otherwise it's regenerated each boot). |
| Stored integration secrets "disappear" after a move | TRIPWIRE_SECRET_KEY changed or wasn't set — encrypted values can't be decrypted. Restore the original key. |
| Screenshots/artifacts 404 in the dashboard | The tripwire-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 TRIPWIRE_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.