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 suite covers the following CapRover behavior. The test selection defines the authoritative file-to-tier mapping.
| Area | Coverage |
|---|---|
| Authentication and configuration | Login and authorization contracts; full app updates and PATCH preservation; projects; themes; system defaults |
| Deployment and runtime | Image, captain-definition, source-upload, detached-build, failure-recovery, runtime-log, and deploy-token paths |
| Routing and networking | HTTP routing, redirects, HTTP authentication, WebSockets, application and global Nginx, custom TCP/UDP ports, and HTTPS |
| Data and server state | Persistent volumes, disk cleanup, backups, system information, and safe restoration of global settings |
| Integrations | One-click applications and repositories, Git webhooks, remote and self-hosted registries, GoAccess, and NetData |
| Specialized environments | HTTPS plus Pro/2FA, multi-node placement, and release-to-edge plus edge-to-edge upgrades |
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.
By default, 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. E2E_PROVISION_WORKER=true adds the optional second droplet
used by the multi-node suite. 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.
| Workflow | Purpose | Infrastructure |
|---|---|---|
e2e-multi-node.yml |
Multi-node worker joining, placement, self-hosted registry, and persistent-volume testing | 2 droplets, 2 certificates |
e2e-ephemeral.yml |
Full fresh-server suite; HTTPS mode also runs Pro and 2FA coverage | 1 droplet, HTTP by default |
e2e-ssl-and-registry.yml |
Dedicated SSL and self-hosted registry coverage | 1 droplet, 4 certificates |
e2e-upgrade.yml |
Release-to-edge and edge-to-edge data-preservation coverage | 1 droplet, HTTP |
e2e.yml |
Run the ordinary non-destructive suite against an existing server you provide | No provisioning |
flowchart TB
ALL["All five workflows<br/>TypeScript checking + 101 unit tests"]
subgraph ORDINARY["Ordinary E2E suite"]
direction TB
FRESH["Fresh Server<br/><code>e2e-ephemeral.yml</code><br/><br/>Ordinary suite<br/>HTTPS mode adds Pro + 2FA"]
EXISTING["Existing Server<br/><code>e2e.yml</code><br/><br/>Smoke + core tests<br/>Strict subset of Fresh Server"]
EXTRA["Fresh-only addition<br/><br/>12 destructive test files"]
FRESH --> EXISTING
FRESH --> EXTRA
end
SSL["SSL + Registry<br/><code>e2e-ssl-and-registry.yml</code><br/><br/>1 dedicated specialized test file<br/>Outside the ordinary suite"]
MULTI["Multi-Node<br/><code>e2e-multi-node.yml</code><br/><br/>1 dedicated specialized test file<br/>Outside the ordinary suite"]
UPGRADE["Upgrade<br/><code>e2e-upgrade.yml</code><br/><br/>Release-to-edge + edge-to-edge<br/>Outside the ordinary suite"]
ALL --> FRESH
ALL --> SSL
ALL --> MULTI
ALL --> UPGRADE
SSL -. shared certificate / registry setup .- MULTI
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 its temporary infrastructure even when the test step fails.
Run it normally to use HTTP without issuing a certificate. Check Enable HTTPS
to issue a real Let's Encrypt certificate, force dashboard HTTPS, run the
ordinary suite over HTTPS, and then run the Pro and 2FA specialized coverage on
the same server. HTTPS runs require the dedicated E2E_PRO_API_KEY secret and
share the certificate-issuing concurrency group with the other certificate
workflows. Local npm run test:ephemeral still defaults to HTTP; setting
E2E_ENABLE_HTTPS=true locally only enables HTTPS provisioning and does not
automatically invoke the separate Pro/2FA npm command. 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 |
E2E_PRO_API_KEY |
Dedicated Pro instance key; required only for HTTPS runs |
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 manual CapRover E2E - SSL and Registry workflow provisions its own
ephemeral server with dashboard HTTPS enabled and runs only
tests/specialized/ssl-and-registry.test.ts. It verifies trusted certificates
for an app's base domain and custom domain, app-level force-SSL redirects, and
the self-hosted registry's API, Docker service, TLS endpoint, default-push
behavior, deletion protections, image contents, and cleanup.
The workflow uses the same six provisioning secrets as the normal fresh-server workflow. Provisioning validates them before creating a droplet. The workflow is manual-only and serialized with other certificate-issuing E2E workflows.
Each complete run requests four Let's Encrypt certificates: dashboard, app,
custom domain, and registry. Let's Encrypt currently permits 50 certificates
per registered domain in a rolling seven-day period.
Manage its four-certificate cost together with the multi-node workflow's
two-certificate cost and the fresh-server HTTPS mode's one-certificate cost.
Keep 4 × SSL runs + 2 × multi-node runs + HTTPS fresh-server runs at or below
40 per rolling seven days for the configured base domain. This leaves room for
interrupted attempts and other HTTPS runs. A failed run may consume part of its
certificate budget.
Dispatch the specialized workflow with:
gh workflow run e2e-ssl-and-registry.ymlThe manual CapRover E2E - Multi-Node workflow provisions a CapRover manager and a separate Docker worker. It enables the self-hosted registry, adds the worker through CapRover's node API, cross-checks node state with Docker Swarm, and verifies that source-built stateless and persistent applications run when pinned to the worker. The persistent case writes data to the worker-local named volume, replaces the task image, and verifies the data remains.
Set E2E_PROVISION_WORKER=true only for a run that needs the second droplet.
Provisioning exports its address as E2E_WORKER_IP, records its identifier for
failure recovery, and destroys it independently from the manager. This workflow
uses the same six provisioning secrets, is manual-only, and shares the
certificate-issuing concurrency group with the SSL workflow.
Each complete multi-node run requests two Let's Encrypt certificates, one for the dashboard and one for the self-hosted registry. Its dispatches count toward the shared certificate budget described above.
Dispatch it with:
gh workflow run e2e-multi-node.ymlThe CapRover E2E - Fresh Server workflow includes
tests/specialized/pro-and-2fa.test.ts whenever Enable HTTPS is checked. The
ordinary fresh-server suite runs first over HTTPS, then the Pro/2FA test claims a
dedicated Pro key, checks subscription state and Pro configuration, requests a
fresh TOTP URI, enables two-factor authentication, verifies login requires an
OTP, and logs in with a generated code. Test cleanup disables 2FA and restores
the initial Pro configuration before infrastructure teardown.
Configure E2E_PRO_API_KEY as an Actions secret containing an instance key
reserved solely for this suite. The Pro service updates the key's associated
hostname on each claim and replaces its stored TOTP secret on setup, so the
same key works across fresh runs. The Pro service retains the most recent
hostname until the next claim; keep the key separate from any live server.
HTTPS fresh-server runs validate the secret before provisioning, share the
certificate-issuing concurrency group with the SSL and multi-node workflows,
and request one dashboard certificate per complete run.
Dispatch the combined HTTPS + Pro/2FA mode with:
gh workflow run e2e-ephemeral.yml -f enable_https=trueThe manual CapRover E2E - Upgrade workflow provisions one HTTP server
running the pinned caprover/caprover:1.15.4 release. It creates an app, project,
and named volume containing a unique marker, then performs two upgrades:
- Docker updates the captain service to edge commit
37fe03e3267d381342da14a6c9bab543590cc4f3, crossing from the released image repository tocaprover/caprover-edge. - The running edge server's
performUpdateAPI updates it to edge commita45cce6b7fc0d30ca431a8d8ce83bef72a5a9913.
Before provisioning, the workflow checks all three images exist, support
linux/amd64, and the two edge tags resolve to distinct linux/amd64 child
manifests. After each upgrade, it
checks the actual running captain container image ID, logs in with a fresh
client, and verifies project membership, app configuration, HTTP routing, and
the volume marker. CapRover's edge images both report internal version 0.0.1,
so the service image and running container provide the build identity.
The A-to-B transition exercises A's update code and B's data compatibility. Testing B's update code against a newer build requires a subsequent published edge commit. Refresh the pinned pair after the next edge publish to exercise B-to-C. SHA tags are retained only for the newest 100 edge commits, so update the pair if preflight reports a missing tag. This workflow is opt-in, shares the normal Fresh Server provisioning secrets, and requests no certificates.
gh workflow run e2e-upgrade.ymlThe 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:specialized:ssl-and-registry |
Controlled SSL and registry file; requires ephemeral mode |
npm run test:specialized:multi-node |
Two-node placement and persistence file; requires ephemeral mode |
npm run test:specialized:pro-and-2fa |
Pro claim and OTP login file; 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 tests under tests/specialized/ are excluded
from all default selections and run only through their explicit workflow modes.
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.
Start from the confirmed CapRover backend and caprover-api contract. Keep each
test focused and independently reviewable.
- Validate important mutations through the API, Docker Swarm state over SSH, and public behavior where applicable.
- Use unique owned resource names, register cleanup before named-resource creation, run cleanup in reverse order, and restore every changed global setting.
- Keep mutating tests serial. Use bounded polling with a concrete terminal condition and set workflow timeouts from measured runtime.
- Preserve the distinct full-update and PATCH paths. PATCH tests must prove that omitted fields remain intact.
- Prefer digest-pinned fixture images. Record the requested server image and resolved running digest when a test depends on a backend version.
- Keep credentials, tokens, decrypted configuration, and backup contents out of assertions and diagnostics.
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.