Self-hosted · Configuration

Storage

Where TestVibe puts artifacts and traces, the two URLs that have to be right, what fills the disk, and how retention behaves on your own hardware. Sign in with the email on your subscription.

We send a 6-digit code. Access is limited to emails on an active self-hosted subscription — talk to sales if you do not have one yet.

We sent a code to . It expires in 10 minutes.

Self-hosted · Configuration

Storage

Two stores, two URLs, one disk that eventually fills up. Read section 02 if traces will not open, and section 05 before you let a busy workspace run unattended for a month.

01

What is stored where

Everything TestVibe owns lives in exactly two places, and on the appliance both of them are on the data disk. There is no third store and nothing goes to a cloud bucket.

The application web service The test runner uploads artifacts Your browser downloads traces PostgreSQL projects, features, specs, runs, users, settings Object store Playwright traces, videos, screenshots, run bundles, asset library, baselines TV_BLOBSERVER_ROOT internal URL public URL On the appliance both live on disk 2 /var/lib/postgresql /var/lib/testvibe/blob Back that disk up and you have backed up everything.
Diagram — the two stores, their three writers, and where they land on the appliance.

The object store on self-hosted

On the cloud, TestVibe uses Azure Blob. Self-hosted swaps that for a small first-party service — the blob server — that stores objects on a plain filesystem path. That swap is already wired for you: TV_STORAGE_PROVIDER=http is set by both shipped deployments and is not something you configure.

DeploymentObjects land inBlob server listens on
Appliance /var/lib/testvibe/blob (disk 2) 0.0.0.0:8090
Docker Compose /data in the blobserver container, backed by the testvibe_blobdata volume container 8080, published as 8080
Kubernetes the persistent volume the chart mounts at TV_BLOBSERVER_ROOT the chart's blobserver service

Objects are keyed accounts/{account}/projects/{project}/runs/{run}/…, so what belongs to which workspace and project is legible on disk — useful when you are sizing or auditing, and the reason per-run and per-project deletion can be exact.

02

The two object-store URLs

This is the configuration that produces the most confusing symptom in the whole product, because when it is wrong everything else keeps working.

The symptom Runs pass, results are complete, and then traces, screenshots and asset previews render as broken links. It reads like the object store is broken. It is not — the object store is fine, and the browser is simply being sent to an address it cannot reach.

Why there are two

A Playwright trace is downloaded by the user's browser, directly from the object store — not proxied through the application. So the store needs one address for machines inside the deployment and a different one for people outside it.

KeyWho uses itTypical value
TV_BLOBSERVER_URL The application and the test runner, inside the deployment — artifact uploads and run bundles. http://127.0.0.1:8090 (appliance)
http://blobserver:8080 (compose)
TV_BLOBSERVER_PUBLIC_URL Your users' browsers — traces, videos, screenshots, asset previews. http://<your-host>:8090 (appliance)
http://<your-host>:8080 (compose)
Mind the port — it differs by deployment The appliance serves objects on 8090. The Compose stack publishes the blob server on 8080 (the app is on 3000). The console's placeholder text shows the appliance form https://testvibe.yourcompany.com:8090 — on Compose substitute 8080, and behind a reverse proxy substitute whatever route you published.

Set it correctly

  1. Appliance: console → AddressesArtifact URL. On first boot the appliance seeds it from the address DHCP handed out, and the field stays editable — which matters, because an appliance has no OS login and a moved lease would otherwise break every trace link permanently. If the address changed, fix it here.
  2. Docker Compose: set TV_BLOBSERVER_PUBLIC_URL in .env. It defaults to http://localhost:8080, which works only for someone sitting at the host — change it as soon as anyone else uses the install.
    .env
    TESTVIBE_PUBLIC_BASE_URL=https://testvibe.example.com
    TV_BLOBSERVER_PUBLIC_URL=https://artifacts.testvibe.example.com

    Pinned in the environment means the console shows both fields locked, which is correct for a config-as-code deployment.

  3. Behind a reverse proxy: publish a route to the blob server and put that route here. If the app is on HTTPS, the artifact URL must be too — a browser on an HTTPS page will refuse to fetch a plain-HTTP trace.
  4. Prove it. Run one test, open the run, and open its trace from a normal user's desktop. Nothing else in the product exercises this path, which is exactly why it goes unnoticed until someone needs a trace.
03

Keys and CORS

The object store is not open. Reads and writes carry short-lived signed tokens, and server-to-server calls carry an admin bearer. Both keys are generated for you.

KeyWhat it protectsRules
TV_BLOBSERVER_SIGNING_KEY The signed ?token= URLs the app hands to a browser or to the runner. Must be identical on the web service and the blob server. Rotating it invalidates outstanding links; since they are short-lived, that is a brief inconvenience rather than data loss.
TV_BLOBSERVER_ADMIN_KEY Server-to-server administrative routes on the blob server. Must be identical on both. Never exposed to a browser.
TESTVIBE_SECRET_PROTECTION_KEY Encryption of stored secrets — connector tokens, SMTP passwords, hook signing secrets, console-stored credentials. You do not set this one. On Compose and Kubernetes the app mints its own key into its own database the first time it needs one, and both the app and the console read it back from there — so your database backup already contains it. .env.example ships the variable commented out and the chart's values.yaml says to leave it empty. If you do set one: never rotate it. Everything encrypted with the old key becomes unreadable.

The appliance generates all three on first boot and stores them on the data disk. The Compose .env.example ships the two blob keys as placeholders you must replace before the first start; the protection key is commented out and should stay that way. On Kubernetes the two blob keys are chart secret values and secrets.protectionKey is deliberately left empty.

Cross-origin is already handled. The trace viewer fetches trace.zip from the object store's origin, which is a different origin from the application. The blob server answers CORS on its public routes so this works out of the box; if you put a proxy in front of it, do not strip those headers.
04

What fills up

Usage is measured per workspace and split into eight domains. In practice one of them dominates and it is always the same one.

DomainWhat it isGrows
Runs & traces Playwright traces, videos, screenshots and the staged run bundle, per run. Fastest, by far
Telemetry Vitals history and application-error records from watched servers. Steadily, while watched
Load testing Load-run samples and sampled on-failure screenshots. Per load run
Generation Generation snapshots and discovery screenshots. Per generation
Assets The asset library, including visual-regression baselines. When people upload
Assistant chat Conversation history. Slowly
Test Features Your features and generated specs — the source of truth. Slowly. Never deletable.
Project notes Per-project notes. Negligible
Sizing rule of thumb A trace-heavy run of a handful of scenarios is typically single-digit to low tens of megabytes. Multiply by your run frequency and your retention window and you have your disk requirement. The appliance ships a 60 GB dynamically-expanding data disk; if you intend to run a full suite nightly, plan to grow it.

Where users see it

  • Settings → Storage (workspace) — the total, a per-domain breakdown with a meter, a "By project" card, and the retention controls. This panel is overview and policy; it has no delete buttons.
  • Settings → Project → Project Storage — where deletion actually happens, scoped to the current project, per domain. Chat and notes require typing the project name to confirm.
  • The Dashboard carries the same usage summary.

Numbers are refreshed by an hourly reconcile, and immediately when the panel is opened or an asset is deleted. A figure that looks stale usually is — reopen the panel.

05

Quota, retention and the reaper

This section is the one to read carefully. Storage quotas are a subscription concept that does not apply to a self-hosted install — but the housekeeping that was built around them is still running, and it does not know that.

What is switched off on self-hosted

The enforcement gates all short-circuit. Nothing is refused because of a quota:

  • New runs, generations and load tests are never blocked by storage.
  • Visual-regression baselines and PDF-import screenshots are never refused.
  • The "Storage full" prompt never appears on a dispatch.
What is NOT switched off — read this before you leave an install unattended

The hourly housekeeping sweep still compares each workspace against a plan quota, and still evicts data when it is over. A self-hosted workspace resolves to the default entitlement — 1 GiB — so a busy install passes it quickly. Once above it, every hour the sweep will:

  • delete the oldest day of telemetry, and
  • delete the oldest artifact-bearing run's trace, video, screenshots and bundle — the run row and its pass/fail result survive, the evidence does not.

It repeats each hour until the workspace is back under 1 GiB. Nothing warns you, and the run list simply stops offering a trace on older runs. If long trace retention matters to you, raise the entitlement before you accumulate data you cannot afford to lose — see the mitigation below — and tell support@testvibe.com that this is your situation.

Mitigations, best first

  1. Raise the storage entitlement in the database. This is the direct fix and it is one additive statement. Back up first, and run it against the TestVibe database.
    psql — raise the entitlement to 500 GB
    -- what each workspace currently resolves to
    SELECT account_id, name, plan_code FROM tv_account;
    
    -- raise the entitlement on the plan(s) actually in use
    UPDATE tv_plan
       SET storage_bytes_included = 536870912000
     WHERE plan_code IN (SELECT DISTINCT plan_code FROM tv_account);

    On Docker Compose: docker compose exec postgres psql -U tvadmin -d testvibe. On Kubernetes, exec into the database pod. On the appliance there is no OS login, so this route is not available to you — use the next two, and contact support.

  2. Set retention deliberately, so you stay under whatever the ceiling is. Settings → Storage → per-domain retention. Runs have no default retention (they are kept until quota pressure — which is precisely what the eviction above is); telemetry defaults to 30 days. Setting an explicit runs retention of, say, 14 days turns an unpredictable eviction into a predictable policy.
  3. Delete deliberately. Settings → Project → Project Storage, per domain. Deletes are never gated, by design — they are the recovery path.
  4. Grow the disk. That solves running out of space; it does not stop the eviction above, which is measured against the entitlement and not against free space.

Two more behaviours that surprise people

  • The meter and the percentage in Settings → Storage are cosmetic on self-hosted. They are drawn against the same 1 GiB entitlement, so a healthy install can legitimately read "12.4 GB of 1.0 GB · 100%". Nothing is being refused because of it — with the one exception below.
  • Asset uploads are the exception, and they are refused. The asset-upload path checks the quota itself rather than going through the exempted gate, so on a self-hosted install over the entitlement, Settings → Assets will refuse to open the file picker and the server will refuse the whole batch with "only N of your 1.0 GB storage quota is free". Raising the entitlement as above is the fix; there is no setting for it.
  • Dormant workspaces on the default entitlement have their telemetry purged after 30 days of inactivity. Runs, features and specs are not touched by that sweep — only telemetry.
06

Reclaiming space

When the disk is genuinely filling, work down this list. Deletes always work, including in the licence read-only state.

  1. Find out which project. Settings → Storage → the "By project" card. It is almost always one project with a nightly full suite.
  2. Clear the biggest domain for that project. Settings → Project → Project Storage → the Runs & traces row. This removes the artifacts and the staged bundles; the run rows and their results stay, so history and trends survive.
  3. Then set a retention policy so you do not do this again. Settings → Storage → the retention field on the same domain.
  4. Check the free space actually moved.
    appliance
    df -h /var/lib/testvibe /var/lib/postgresql
    du -sh /var/lib/testvibe/blob
    docker compose
    docker system df -v | grep testvibe_
    docker compose exec blobserver du -sh /data
What you cannot delete: Test Features. Your features and generated specs are the source of truth and there is deliberately no bulk-clear for them — delete individual features in the application if you mean it.

Backups

  • Appliance: the data disk is the backup unit. Snapshot or back up disk 2 and you have the database, every artifact, the secrets, the install ID and the licence. The OS disk holds nothing of yours.
  • Compose / Kubernetes: back up the Postgres volume and the blob volume together. Restoring one without the other gives you runs whose traces 404, or orphaned files nothing references.
  • Appliance only: keep a copy of TESTVIBE_SECRET_PROTECTION_KEY with the backup. On Compose and Kubernetes the key lives inside the database, so your database backup already has it.
  • Backup and restore has the step-by-step procedure for each shape, including the restore ordering that avoids the mismatch above.
07

Troubleshooting

SymptomAlmost alwaysFix
Runs pass but the trace will not open; screenshots are broken images. TV_BLOBSERVER_PUBLIC_URL points somewhere the browser cannot reach — the internal name, localhost, or the wrong port. Section 02.
Traces open for you on the host, not for anyone else. The public URL is http://localhost:… — correct only from the host itself. Set it to the address your users use.
Mixed-content or CORS errors in the browser console on the trace viewer. The app is on HTTPS and the artifact URL is on plain HTTP, or a proxy is stripping CORS headers. Serve both over the same scheme; leave the blob server's CORS headers intact.
Runs fail with upload errors. The internal URL or the signing/admin keys disagree between the web service and the blob server. Section 03 — the keys must be identical on both.
Older runs quietly lose their traces. The over-entitlement eviction described in section 05. Raise the entitlement and set an explicit retention policy.
Asset upload refused with a quota message on a box with plenty of disk. The asset path is not exempt from the quota check. Section 05, mitigation 1.
"Runs & traces 0 B" while the disk is clearly full. The reconcile has not run since a bulk change. Reopen Settings → Storage, which forces a reconcile, or wait an hour.

Where to go next