Self-hosted · any test framework · one Docker host
Tests belong to the application. Execution belongs to TestFleet.
Ship your end-to-end suite as a container image. TestFleet schedules it, runs it in isolation against each environment, streams its output, keeps its results, and tells you when something breaks.
Running 44 tests using 4 workers
✓ login › signs in with single sign-on (2.1s)
✓ checkout › pays with card (4.8s)
✓ checkout › applies a voucher (3.2s)
- search › filters by region
✓ account › updates the address (1.9s)
POST /api/orders Authorization: Bearer [MASKED]
41 passed, 3 skipped (2.4m)
Runs any suite that fits in a container
- Playwright
- Cypress
- Selenium
- pytest
- Jest
- xUnit
- k6
- a shell script
Schedules that keep time
Cron in the time zone you mean, daylight saving handled, missed slots coalesced, and an overlap policy for slow suites.
Live output
Follow the suite line by line in the browser. Every line is stored, so the log is still there tomorrow.
Secrets stay secret
Variables are encrypted at rest, never sent back to the browser, and masked in the log before it is stored.
Results and artifacts
JUnit XML becomes per-test results. Screenshots, videos, traces, and HTML reports are kept with the run.
Alerts on change
Email, Slack, Teams, or a signed webhook when a suite starts failing or recovers, not on every red run.
Limits that protect
Timeouts, CPU and memory limits, and concurrency per environment, so a suite never floods production.
Built for pipelines
Set the image tag, start a run, stream its log, and fail the build from CI, with one script.
Survives restarts
Suites run in their own containers. When TestFleet restarts, it picks them up again, deadline and all.
The contract
Your suite stays yours. It only needs to fit in a container.
The suite lives in the application’s repository, versioned with the code it tests. TestFleet asks for very little:
- configuration arrives as environment variables
- exit code
0means passed - logs go to stdout and stderr
- JUnit XML and any other files go to
/TestFleet/artifacts/
That is the whole contract. Playwright, Cypress, pytest, or a shell script all fit.
FROM mcr.microsoft.com/playwright:v1.56.0-nobleWORKDIR /suiteCOPY package*.json ./RUN npm ciCOPY . .RUN mkdir -p /TestFleet/artifacts && chown -R pwuser /TestFleetUSER pwuserCMD ["npx", "playwright", "test"]BASE_URL=https://staging.portal.example.com # from the environmentAPI_TOKEN=•••••••• # a secret, masked in logsTestFleet_RUN_ID=1842 # set by TestFleetTestFleet_ENVIRONMENT=stagingTestFleet_ARTIFACTS_DIR=/TestFleet/artifactsOne pipeline
Run now, on a schedule, or from CI. Same path every time.
A click, a cron schedule, and an API call all create the same queued run. The dispatcher starts it when the global limit and the environment’s own limit allow, so a burst of runs never overloads the host or the environment they test.
Each run records the exact image digest it used, so a run from last month can be traced to what it actually tested.
- 1Queued runEvery trigger creates the same record
- 2DispatcherAdmits it under the global and per-environment limits
- 3ContainerPulled, started, hardened, watched until it exits
- 4ResultExit code and JUnit decide the status; artifacts are kept
Honest results
A failing test is not a broken registry.
TestFleet keeps failed (your tests found a problem) apart from error (TestFleet could not run them). An unreachable registry, a missing image, or a container killed for memory never shows up as a red test.
The exit code and the JUnit report decide together: a suite that crashes in its setup, or swallows its own exit code, still gets the right status.
On its way
- queuedWaiting for a free slot under the limits
- preparingPulling the image, creating the container
- runningThe suite is executing
About your application
- passedEvery test passed
- failedThe suite ran, and tests failed
- timeoutIt ran longer than its timeout
About the infrastructure
- errorTestFleet could not run it: registry, image, Docker, memory
- cancelledSomeone stopped it
CI integration
Deploy, test, and gate the release in one step.
After a deployment, the pipeline points the test definition at the matching E2E image, starts a run, streams its log into the job, and fails the job unless the run passed. One script for POSIX shells, one for PowerShell.
e2e: needs: deploy-staging runs-on: [self-hosted] concurrency: staging steps: - uses: actions/checkout@v4 - run: sh ci/testfleet-run.sh customer-portal e2e staging "${{ github.ref_name }}" env: TESTFLEET_URL: https://testfleet.example.internal TESTFLEET_TOKEN: ${{ secrets.TESTFLEET_TOKEN }}TestFleet: e2e now uses ghcr.io/acme/portal-e2e:1.4.2TestFleet: run 1842 of ghcr.io/acme/portal-e2e:1.4.2 on staging…the suite's output, as it runs…TestFleet: run 1842 passed (41 passed, 0 failed, 3 skipped)Self-hosted
Your network, your data, one Docker host.
TestFleet runs as three containers: the app, PostgreSQL, and a Docker socket proxy that only allows the calls TestFleet needs. Test containers run on their own network and cannot reach TestFleet’s database.
It runs on any 64-bit Linux host with Docker, amd64 or arm64, down to a Raspberry Pi. Log in with your company’s identity provider over OpenID Connect, or with passwords and invitations.
curl -fsSLO https://raw.githubusercontent.com/TestFleetLabs/TestFleet/main/deploy/compose.yamlcurl -fsSL -o .env https://raw.githubusercontent.com/TestFleetLabs/TestFleet/main/deploy/.env.example# fill in PHX_HOST, SECRET_KEY_BASE, CLOAK_KEY, POSTGRES_PASSWORDdocker compose up -dGive your suites a home
Three containers on one Docker host, behind your reverse proxy. A Compose file, four values in .env, and your first suite is running.