Skip to content

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.

Run #1842 · customer-portal / e2e / staging
passedghcr.io/acme/portal-e2e:1.4.2via CI pipeline

					
					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)
				
41 passed0 failed3 skippedjunit.xml · playwright-report/ · 2 traces

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 0 means 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.

Read the container contract →

Dockerfile
FROM mcr.microsoft.com/playwright:v1.56.0-noble
WORKDIR /suite
COPY package*.json ./
RUN npm ci
COPY . .
RUN mkdir -p /TestFleet/artifacts && chown -R pwuser /TestFleet
USER pwuser
CMD ["npx", "playwright", "test"]
What the container gets
BASE_URL=https://staging.portal.example.com # from the environment
API_TOKEN=•••••••• # a secret, masked in logs
TestFleet_RUN_ID=1842 # set by TestFleet
TestFleet_ENVIRONMENT=staging
TestFleet_ARTIFACTS_DIR=/TestFleet/artifacts

One 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.

Run nowScheduleAPI
  1. 1
    Queued runEvery trigger creates the same record
  2. 2
    DispatcherAdmits it under the global and per-environment limits
  3. 3
    ContainerPulled, started, hardened, watched until it exits
  4. 4
    ResultExit 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.

How the status is decided →

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.

Run tests from a pipeline →

.github/workflows/deploy.yml
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 }}
Job output
TestFleet: e2e now uses ghcr.io/acme/portal-e2e:1.4.2
TestFleet: 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.

Install TestFleet →

On the server
curl -fsSLO https://raw.githubusercontent.com/TestFleetLabs/TestFleet/main/deploy/compose.yaml
curl -fsSL -o .env https://raw.githubusercontent.com/TestFleetLabs/TestFleet/main/deploy/.env.example
# fill in PHX_HOST, SECRET_KEY_BASE, CLOAK_KEY, POSTGRES_PASSWORD
docker compose up -d

Give 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.