External end-to-end tests for CapRover. The suite can run against an existing disposable server or provision a fresh DigitalOcean server and Cloudflare DNS record for a clean-room run.
The suite validates each lifecycle change from three independent perspectives:
- CapRover API state through the published
caprover-apipackage - Docker Swarm state over SSH
- Publicly observable HTTP behavior
The initial suite runs one sequential application lifecycle:
- Validate CapRover, SSH, Docker, and Swarm manager access.
- Create an app and wait for its placeholder service.
- Rename the app.
- Add and verify an environment variable.
- Deploy a pinned Alpine-based Nginx image.
- Scale to two instances.
- Scale back to one instance.
- Deploy a different pinned Nginx image.
- Delete the app and verify its Nginx response disappears.
The suite supports the current <appName> Docker service naming and the legacy
srv-captain--<appName> naming used by older CapRover installations.
- Node.js 22.12 or newer
- For existing-server runs: a disposable CapRover installation with a configured root domain
- Public wildcard DNS for application subdomains
- Port 80 reachable from the test runner (and port 443 for HTTPS runs)
- SSH access to the Docker Swarm manager
- Docker access for the configured SSH user
For existing-server runs, the CapRover URL must use HTTPS and be the dashboard origin, for example:
https://captain.example.com
Do not include /api/v2, a trailing path, query parameters, or a fragment.
Install dependencies:
npm ciTo run against an existing disposable CapRover server, create your local environment file and fill in the server values:
cp .env.template .env
npm test.env is gitignored and is loaded automatically only for local runs. CI systems
such as GitHub Actions provide their environment variables directly and do not
load .env.
The SSH host defaults to the hostname from CAPROVER_URL. Set SSH_HOST
explicitly only when SSH is exposed through a different hostname or IP address.
SSH_PORT is optional and defaults to 22.
To provision a fresh server, run the tests, and destroy the temporary
infrastructure in one command, fill in the ephemeral provisioning values in
.env and run:
npm run test:ephemeralThe workflow uses npm run provision and npm run destroy as lower-level
commands. For local end-to-end runs, prefer npm run test:ephemeral so the
generated connection details are passed directly to the test process.
provision creates one DigitalOcean droplet, creates a unique unproxied
Cloudflare wildcard DNS record, verifies Docker is available, starts a fresh
CapRover instance, configures its root domain, and generates a temporary
CapRover password. The default DigitalOcean image has Docker preinstalled; custom
images still use the existing Docker installation fallback when needed. The generated cleanup state is stored locally in
.e2e-provisioning-state.json and is gitignored.
See Provisioning design for the full lifecycle, failure-recovery behavior, credential flow, and code layout.
The default provisioning configuration uses nyc3, s-1vcpu-2gb,
docker-20-04, and caprover/caprover-edge. These can be overridden with
DIGITALOCEAN_REGION, DIGITALOCEAN_SIZE, DIGITALOCEAN_IMAGE, and
CAPROVER_IMAGE.
The test output never prints the CapRover password or SSH private key. Failure diagnostics include sanitized CapRover state, Docker service state, task state, and a bounded tail of logs from the generated test application.
The existing CapRover E2E workflow runs manually through Actions → CapRover E2E → Run workflow and reuses an already-provisioned server.
Configure these repository secrets first:
| Secret | Description |
|---|---|
CAPROVER_E2E_PASSWORD |
Password configured on the disposable CapRover server |
CAPROVER_E2E_SSH_PRIVATE_KEY |
Private key matching an authorized key on the server |
Each run asks for:
- CapRover dashboard URL
- SSH user
- SSH port
The SSH host is derived from the CapRover dashboard URL. The workflow only
supplies configuration and runs npm test; all test logic lives in the
TypeScript suite.
The CapRover E2E - Fresh Server workflow provisions a new environment, runs the smoke, core, and ordinary destructive suites, and destroys the temporary DNS record and droplet even when the test step fails.
Run it normally to use HTTP without issuing a certificate. To exercise the
dashboard SSL setup, check Enable HTTPS when dispatching the same workflow.
This issues a real Let's Encrypt certificate, forces dashboard HTTPS, and runs
the same suite over HTTPS. Local npm run test:ephemeral also defaults to HTTP;
set E2E_ENABLE_HTTPS=true in .env to opt in. Only true and false are
accepted. Both modes use HTTP for application subdomains unless a test explicitly
enables SSL on an app.
Configure these repository secrets:
| Secret | Description |
|---|---|
DIGITALOCEAN_TOKEN |
DigitalOcean API token with droplet access |
DIGITALOCEAN_SSH_KEY_ID |
DigitalOcean ID of the public key matching the SSH private key |
CLOUDFLARE_API_TOKEN |
Cloudflare API token with DNS edit access |
CLOUDFLARE_ZONE_ID |
Cloudflare zone ID containing the E2E base domain |
E2E_BASE_DOMAIN |
Base domain under which temporary wildcard records are created |
CAPROVER_E2E_SSH_PRIVATE_KEY |
Private key matching the DigitalOcean SSH key |
The fresh-server workflow uses a generated CapRover password for each run. The existing-server workflow remains available for fast repeated test runs without reprovisioning infrastructure.
The standard CapRover E2E - Fresh Server workflow includes
tests/git-webhooks.test.ts in its destructive tier. Persistent-server runs
continue to exclude it.
Run the setup script from a local checkout to create the dedicated private
repository, populate its fixture commit, install a read-only SSH deploy key, and
set all seven E2E_GIT_* Actions secrets:
./scripts/setup-git-fixture.shThe script requires gh, git, ssh, and ssh-keygen. Authenticate gh with
an account that can create the fixture repository and administer Actions
secrets in caprover/caprover-e2e. GitHub does not expose an API for creating a
fine-grained personal access token, so the script prompts without echoing for a
token with read-only Contents access to the fixture repository. You can also
provide it as CAPROVER_E2E_GIT_HTTP_TOKEN. The explicit --use-gh-token
fallback uses the current GitHub CLI token, which may have access to more than
the fixture repository. Run ./scripts/setup-git-fixture.sh --help to override
the repository names or branch.
The resulting private repository contains the file
tests/fixtures/git-webhook-repo/captain-definition
at its root on the configured branch. Pin the branch to that fixture commit
while running this workflow. Give CapRover read access through both a
fine-grained read-only GitHub token and a read-only deploy key. Use a separate
key from the one used to SSH into the DigitalOcean server. No GitHub webhook
needs to be installed: the test sends the GitHub push payload directly to
CapRover and verifies the resulting Git clone and deployment.
Configure the fresh-server secrets listed above and these additional secrets
in the caprover-e2e repository:
| Secret | Value |
|---|---|
E2E_GIT_HTTPS_REPO |
https://github.com/owner/private-fixture.git |
E2E_GIT_SSH_REPO |
git@github.com:owner/private-fixture.git |
E2E_GIT_BRANCH |
Branch containing the pinned fixture commit |
E2E_GIT_HTTP_USER |
Git username, commonly x-access-token |
E2E_GIT_HTTP_PASSWORD |
Read-only fine-grained GitHub token for that repository |
E2E_GIT_SSH_PRIVATE_KEY |
Unencrypted private key for the read-only deploy key |
E2E_GIT_EXPECTED_COMMIT |
Full 40-character SHA of the fixture commit |
The fresh-server workflow checks that settings exist before creating a droplet. The test also checks both URLs point to the same repository and compares the Git hash from each build to the expected commit. Git credentials and webhook tokens are never included in assertions. Failure diagnostics redact raw, newline-expanded, and URL-encoded credential values before printing captain logs. Dispatch the normal fresh-server workflow with:
gh workflow run e2e-ephemeral.ymlnpm run typecheck
npm run formatThe test creates unique application names and performs best-effort cleanup for both the original and renamed names. Cleanup warnings preserve the original test failure.
| Command | Selection |
|---|---|
npm test |
Type checking, then test:all |
npm run test:unit |
Local unit tests, no server required |
npm run test:smoke |
Existing application lifecycle |
npm run test:core |
Explicitly listed app-scoped tests on a dedicated test server |
npm run test:destructive |
Explicitly listed global and destructive tests; requires ephemeral mode |
npm run test:all |
Unit, smoke, core; adds ordinary destructive tests in ephemeral mode |
Provisioning sets CAPROVER_E2E_ENVIRONMENT=ephemeral for the test process.
Leave this unset for existing servers. This flag declares a disposable environment;
set it only for a freshly provisioned server owned by the run. Never point the suite
at a production server. Each future destructive file must call requireEphemeral()
before creating a context or mutating resources. Direct file filters cannot expand
the selected tier. Specialized workflows under tests/specialized/ are excluded
from all default selections and have their own explicit configuration. Git webhook
coverage is part of the ordinary destructive tier because its fixture prerequisites
are validated before fresh-server provisioning.
Core and destructive commands fail with no tests until their files are implemented. Existing-server workflow runs are serialized without cancelling an active run. Coordinate local runs separately to avoid concurrent mutations of the same server.
New resource tests use withCleanup(): register cleanup before a named resource's
create request, scope it to that run's exact resource, and tolerate an already-absent
resource. Cleanup runs in reverse order and attempts all actions. A cleanup failure
fails the run, and an original test failure is retained alongside cleanup failures.
The existing smoke lifecycle keeps its original best-effort cleanup behavior.
Provisioning logs the requested CapRover image, and environment validation logs
the running service image reference including its digest when Docker resolves one.
For reproducible prerequisite validation, set CAPROVER_IMAGE to a known digest
containing the required backend fix and retain the run's image output.
Authentication coverage exercises valid login, empty/oversized password validation, one wrong-password attempt, SDK error propagation, and an unauthenticated request. Rapid repeated runs can encounter the server's global failed-login backoff; wait for that window to expire before retrying. Root-domain setup and password change are covered by every fresh-server run; root SSL and global force SSL are covered when Enable HTTPS is checked.
Source uploads require caprover-api@0.0.22 or newer, which includes the merged
native FormData fix.
The fixture uses the runner's tar executable and a digest-pinned Nginx image;
it creates unique HTTP and Unicode startup-log markers without extra dependencies.