Deployment
Stand up a protected Worker without losing local mode
Hosted Fleet uses a custom-domain Worker, Cloudflare Access, D1, and encrypted Worker secrets. Generated deployment and operator files stay ignored; the committed defaults are safe, read-only, and unscheduled.
Prerequisites
- Node.js 22 or newer and npm
- A Cloudflare account with an active zone for the Fleet hostname
- Permission to create Workers, a D1 database, a custom domain, and a Cloudflare Access application
- An account API token scoped to the resources Fleet should inspect and, if enabled, change; see the least-privilege starting point
- A configured Cloudflare Zero Trust team domain and identity provider
Begin read-only
The configuration generator defaults to FLEET_READ_ONLY=true. Inspect the deployed inventory and Access boundary before regenerating with --write.
Hosted path
Provision the Worker
1
Install the pinned toolchain
Choose a tagged release with a cloudflare-fleet-VERSION-self-hosted.tgz asset and its SHA-256 file. This archive includes the dependency lock, deployment source, migrations, and a server release identity. The CLI-only package and GitHub's automatic source downloads are different artifacts; earlier tags without a self-hosting asset cannot use these release checks.
release_version="REPLACE_WITH_RELEASE_VERSION"
release_url="https://github.com/j-256/cloudflare-fleet/releases/download/v${release_version}"
release_archive="cloudflare-fleet-${release_version}-self-hosted.tgz"
mkdir "cloudflare-fleet-${release_version}"
cd "cloudflare-fleet-${release_version}"
curl --fail --location --remote-name "$release_url/$release_archive"
curl --fail --location --remote-name "$release_url/$release_archive.sha256"
shasum -a 256 -c "$release_archive.sha256" && tar -xzf "$release_archive"
cd package
npm ci --include=dev
npm run fleet -- --help
Stop on any failed command; do not extract an archive whose checksum fails. Obtain both files from the trusted release channel: checksums detect corruption, not a malicious publisher. Run deployment commands from this extracted package directory. Use its own npm run fleet -- command so a global CLI cannot generate paths into a different installed version. Install the matching CLI-only package separately on operator devices using the getting-started guide.
2
Create the D1 database
Record the database identifier returned by Wrangler. The generated configuration binds it as FLEET_DB.
npx wrangler d1 create cloudflare-fleet
export CLOUDFLARE_FLEET_D1_DATABASE_ID="the-returned-database-id"
3
Create the Access application
In Cloudflare Zero Trust, create a self-hosted application for the exact Fleet hostname and attach an Allow policy for the intended operators. Copy the Application Audience tag from the application settings and use the full HTTPS team domain as the issuer. The Worker validates both values on every non-loopback request.
Cloudflare documents the flow in Publish a self-hosted application and explains the audience and issuer in Validate JWTs.
4
Generate the ignored Wrangler configuration
Use an exact custom-domain hostname with no existing CNAME. Cloudflare creates the DNS record and certificate when Wrangler deploys the custom domain.
export CLOUDFLARE_ACCOUNT_ID="your-account-id"
export CLOUDFLARE_ACCESS_AUD="your-access-application-audience"
export CLOUDFLARE_ACCESS_TEAM_DOMAIN="https://your-team.cloudflareaccess.com"
npm run fleet -- hosted configure \
--account-id "$CLOUDFLARE_ACCOUNT_ID" \
--database-id "$CLOUDFLARE_FLEET_D1_DATABASE_ID" \
--hostname fleet.example.com \
--access-aud "$CLOUDFLARE_ACCESS_AUD" \
--access-team-domain "$CLOUDFLARE_ACCESS_TEAM_DOMAIN"
The command writes mode-restricted wrangler.jsonc. That file contains account and deployment identifiers; keep it private and outside version control. wrangler.example.jsonc documents the portable shape without live values. Operator state and policy belong in their per-user directories, not inside the release; the release checker permits only the standard generated configuration, secret files, dependencies, and build outputs alongside its immutable source.
The generated Workers Standard configuration limits each invocation to 3,000 ms of CPU and 1,000 total subrequests. Workers Observability for the seven days ending 2026-09-07 recorded a maximum of approximately 986 ms CPU for the hosted Worker, leaving about three times the observed peak. The subrequest ceiling preserves broad, bounded account audits while reducing the Paid default from 10,000. If an invocation exceeds either ceiling, Cloudflare terminates it and records a resource-limit outcome; inspect the correlated audit and activity receipt before retrying or raising a limit.
Custom limits.cpu_ms and limits.subrequests require the Workers Standard usage model. A Free deployment must omit that block and instead receives Cloudflare's fixed per-invocation ceilings of 10 ms CPU, 50 external subrequests, and 1,000 internal-service subrequests. The observed CPU maximum exceeds the Free allowance, and a deep fleet audit can fan out across many account APIs, so no Free-compatible hosted audit profile is claimed. Running only smaller standalone read operations is the supported Free-oriented fallback, at the cost of no shared hosted control plane or complete deep-audit guarantee. These limits and the fallback were verified against Workers limits and Wrangler limits configuration on 2026-09-07.
Hosted facet-intent planning and application also read the requested surfaces across the account before saving intent. The request bound does not guarantee Free compatibility: for example, accepting settings, DNSSEC, Email routes, and Workers routes across sixteen zones projects approximately 65 external subrequests per invocation, assuming one zone-list page and one page per surface per zone. That exceeds the Free ceiling before retries or additional pagination; ruleset details can add more requests. This is a workload estimate, not a measured Free deployment. Use the standalone MCP backend with local intent storage to avoid Worker invocation limits, accepting that its intent is separate from hosted D1. Workers Standard supports the shared hosted workflow's broader reads. The per-invocation subrequest limits were verified on 2026-09-18.
5
Apply storage and deploy the secret with the code
Wrangler accepts an ignored dotenv file through --secrets-file, allowing the first code version and encrypted secret to deploy together. Delete the local secret file after deployment if the shell environment is your preferred source of truth.
npm run build:hosted
npm run fleet -- hosted check --version "$release_version"
npm run db:migrate:remote
npm run fleet -- hosted check --version "$release_version" --live --install
umask 077
printf 'CLOUDFLARE_API_TOKEN="%s"\n' "$CLOUDFLARE_API_TOKEN" > .dev.vars.production
npm run deploy -- --secrets-file .dev.vars.production
Proceed only when the checks pass. Offline preflight verifies the selected archive, pinned Wrangler, built assets, account, database, Access configuration, and release-local paths. Live preflight also reads serving deployment and D1 migration metadata. --install permits a Worker that has not been created yet; it does not permit missing database migrations or denied reads. No check applies migrations or deploys code. The custom domain is the Worker origin; the Worker serves no workers.dev endpoint and disables preview URLs.
6
Verify Access and inventory
Open the configured hostname in a normal browser. Access should authenticate first, then Fleet should report a read-only session and begin loading the account. Test an unauthenticated private window as well: the Worker itself must reject a request that arrives without a valid Access assertion.
Configure the hosted client origin and scoped Access credentials, then verify the exact selected release:
npm run fleet -- hosted verify --version "$release_version"
Verification requires one serving version with the expected D1 binding, applied migration names, authenticated storage readiness, matching account and read/write policy, and the archive's server release identity. It also checks that an anonymous API request is denied or redirected to the configured Access provider. It does not replace a human check of login and inventory, prove that every Access policy is correct, or prevent a concurrent deployment.
7
Opt into reviewed writes
After reviewing the account boundary, Access policy, proxy allowlist, and read-only inventory, regenerate the same ignored file with --write and deploy again.
npm run fleet -- hosted configure \
--account-id "$CLOUDFLARE_ACCOUNT_ID" \
--database-id "$CLOUDFLARE_FLEET_D1_DATABASE_ID" \
--hostname fleet.example.com \
--access-aud "$CLOUDFLARE_ACCESS_AUD" \
--access-team-domain "$CLOUDFLARE_ACCESS_TEAM_DOMAIN" \
--write
npm run fleet -- hosted check --version "$release_version" --live
npm run deploy
npm run fleet -- hosted verify --version "$release_version"
Upgrade deliberately, keeping recovery separate
- Read the selected release's notes and migration SQL. Download and verify its self-hosting archive into a new directory using the installation steps above; keep the previous release and private configuration intact. Do not unpack over a running installation or reuse its
node_modules.
- Run
npm ci --include=dev and regenerate configuration using the new archive's CLI. Preserve the existing Worker name, account, D1 ID, Access audience and team, hostname, policy exceptions, and deliberate read/write setting. Do not create a replacement database. A copied configuration is safe only if all deployment paths resolve into the new release; preflight rejects paths back into the old one.
- Stop writers and concurrent deployments. Save private logical state exports, a full D1 export, and the preceding serving-version receipt outside both release directories. Confirm the backups are readable and retain the preceding release's configuration and secret recovery source.
- Build and check the new release. Live preflight reports pending or unexpected migration names and exits nonzero when they differ. Review that result before applying migrations. Unexpected names can indicate an attempted downgrade; never edit the migration history to make a check pass.
- Apply the reviewed migrations, rerun live preflight, and explicitly deploy. Verify the selected release afterward, check human login and inventory, and only then resume writers and update remote CLI/MCP clients to the matching version.
# Inside the new release, with its regenerated configuration:
umask 077
backup_root="${XDG_STATE_HOME:-$HOME/.local/state}/cloudflare-fleet/backups"
mkdir -p "$backup_root"
backup_dir="$(mktemp -d "$backup_root/upgrade.XXXXXX")"
cloudflare-fleet state export --format json > "$backup_dir/hosted-state.json"
npx wrangler d1 export FLEET_DB --remote --output "$backup_dir/database.sql"
npm run build:hosted
npm run --silent fleet -- hosted check --version "$release_version" --live --format json > "$backup_dir/preflight.json"
# After inspecting backups, the preflight result, and migration compatibility:
npm run db:migrate:remote
npm run fleet -- hosted check --version "$release_version" --live
npm run deploy -- --message "Cloudflare Fleet ${release_version}"
npm run --silent fleet -- hosted verify --version "$release_version" --format json > "$backup_dir/verified.json"
The example creates a unique mode-restricted backup directory under your per-user Fleet state directory; include it in your durable private backup system before deployment. Export any independently used standalone state separately with CLOUDFLARE_FLEET_BACKEND=local. Do not publish receipts: they include operator deployment identifiers. Preflight is a point-in-time observation, not a deployment lock or approval. The checks inspect migration names, not schema/data equivalence; CI separately exercises fresh local D1 and a legacy-schema upgrade with preserved records using the exact distributed artifact.
If verification fails, leave writers stopped and investigate using the receipt's correlation ID and per-check failures. Recheck after propagation when appropriate; do not automatically redeploy or restore a database. An operator may explicitly use the recorded previous version with npx wrangler rollback VERSION_ID only after reviewing its schema, binding, secret, policy, and schedule compatibility. Worker rollback does not undo D1 migrations or restore its data. Database recovery is a separate reviewed operation using a private export or the D1 recovery facilities. Revalidate account, storage, Access, and the recovered version before resuming writes.
CLI and MCP release checks
hosted check is offline by default; --live adds Cloudflare reads. hosted verify always performs live reads and authenticated server verification. The local operator token needs Workers Scripts Read and D1 Read; verification additionally needs the application's Access credentials. These helpers do not grant deployment or migration permissions and do not change production credentials. Ordinary hosted client operations still require only Access credentials.
For a CLI installed separately, use --release-dir /absolute/path/to/package and optionally --config /absolute/path/to/wrangler.jsonc. MCP exposes check_hosted_release with an explicit version and optional live/install, and verify_hosted_release with an explicit version. Set the operator-owned CLOUDFLARE_FLEET_RELEASE_DIR and optional CLOUDFLARE_FLEET_WRANGLER_CONFIG in the launching process environment; restart an existing MCP process when those selections change. Tools cannot select arbitrary filesystem paths, fetch source, or perform deployment writes.
Text is the CLI default; --format json includes source identity, coverage, correlation ID, and per-check timings. Exit statuses are 0 for ready/help, 2 for usage, 3 for a missing or mismatched toolchain, 4 for blocked preflight, and 7 for failed post-deployment verification; unexpected runtime failures use 1. Missing metadata is a failure, not a clean bill of health. JSON generated by hosted configure is supported; custom environments and commented JSONC need manual review.
Optional gated production deployment
CI deployment is an opt-in convenience for an existing installation, not a prerequisite for self-hosting. Complete the installation and Access setup first. The production job consumes the exact self-hosting archive tested by the same workflow run, verifies its checksum and source revision, installs the locked dependencies, and builds assets before receiving production credentials. It never provisions a Worker or database, applies migrations, uploads runtime secrets, or publishes a GitHub Release.
Protect main with the repository's required verification checks. Create a GitHub Actions environment named production, restrict its deployment branch to main, and add any required reviewers appropriate for your installation. Configure these values inside that environment:
| Environment value | Type and purpose |
CLOUDFLARE_ACCOUNT_ID | Variable selecting the existing deployment account |
CLOUDFLARE_FLEET_PRODUCTION_URL | Variable containing the exact HTTPS application origin, matching the configuration's custom domain |
CLOUDFLARE_WORKERS_DEPLOY_TOKEN | Secret containing a dedicated deployment token with Workers Scripts Write and D1 Read in the selected account |
CLOUDFLARE_FLEET_DEPLOYMENT_CONFIG | Secret containing the complete reviewed JSON from hosted configure, including the existing Worker name, database, hostname, Access settings, policy, read/write mode, and execution limits |
CLOUDFLARE_FLEET_ACCESS_CLIENT_ID and CLOUDFLARE_FLEET_ACCESS_CLIENT_SECRET | Secrets containing a dedicated service-token pair authorized by this application's exact-token Service Auth policy |
The helper replaces only source, asset, and migration paths with locations inside the selected release. It rejects custom or commented JSONC, unexpected bindings, a changed account or database, a different domain attachment, enabled alternate URLs, and configuration drift. Preserve the deployed read/write mode and policy when generating the reviewed configuration. Store configuration directly from a private file, without putting its contents in command arguments:
gh secret set CLOUDFLARE_FLEET_DEPLOYMENT_CONFIG --env production < /absolute/private/path/to/wrangler.jsonc
gh variable set CLOUDFLARE_FLEET_DEPLOY_PRODUCTION --body true
The opt-in variable is repository-level because it gates entry into the environment. Keep credential values out of tracked files and shell history; use the environment settings or protected standard input. Preserve encrypted recovery copies outside the repository, record credential expiry, and rotate the dedicated credentials independently of human and local CLI access. Never copy the application's runtime Cloudflare token or a general administrative machine credential into Actions.
Cloudflare documents the required Worker read access and D1 query permissions. The deployment token does not need D1 Write, DNS Write, or Access policy administration. Workers Scripts Write still grants sensitive code-deployment authority within its account scope; protect it accordingly. The Access service credential is scoped to the application, not automatically to a read-only role inside Fleet, so treat it as operator access even though this job only sends status requests. Review GitHub's deployment protection controls before enabling automation.
After opt-in, pushes to protected main deploy only when the complete verification job succeeds. Pull requests, forks without explicit configuration, tag releases, and verification-only dispatches do not deploy. The optional manual path reruns all verification before redeployment:
gh workflow run ci.yml --ref main -f operation=deploy-production
Deployment jobs are serialized and do not cancel an in-flight upload. Before uploading, the helper verifies the protected main head, existing authenticated application, Access denial for anonymous callers, binding and settings agreement, and absence of pending or unexpected migrations. It rechecks main and the serving deployment after the dry run. These reads reduce stale-deployment risk but do not lock out a separate human or external deployment, so do not deploy concurrently from another tool.
Wrangler runs from the verified release with --keep-vars, no replacement variable values, and no domain or schedule declarations. The existing runtime token remains a server-side secret. Post-deployment checks require the selected source/content identity, healthy D1 storage, preserved account and read/write mode, and the same Access boundary. Bounded propagation retries are read-only; the job never retries an upload or performs automatic rollback.
For pending migrations, pause deployment and follow the private backup and migration review procedure above, then dispatch a new verification/deployment run. For a failed or uncertain upload, inspect the serving Worker before retrying; use the stage correlation ID and safe failure codes in the job log. Public logs omit raw provider output, operator configuration, and state. A failed post-deployment check may mean the new version is already serving. Review the previous serving version and schema compatibility before an explicit recovery, and keep database recovery separate. Disable automatic deployment by setting CLOUDFLARE_FLEET_DEPLOY_PRODUCTION to false; revoke credentials separately if exposure is suspected.
Operator policy exceptions
Fleet ships with no policy exceptions. If one zone intentionally uses an exact Email DNS record that differs from the derived fleet baseline, copy fleet-policy.example.json to the per-user Fleet configuration directory and edit the relevant fields. Local launches load it directly; cloudflare-fleet hosted configure validates and embeds the same document into the ignored Wrangler variables.
umask 077
fleet_config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/cloudflare-fleet"
mkdir -p "$fleet_config_dir"
cp fleet-policy.example.json "$fleet_config_dir/fleet-policy.json"
cloudflare-fleet hosted configure ...
An Email DNS exception does not hide the matrix difference. It removes only the exact allowed variant from actionable policy drift.
Import existing local intent, activity, and Worker records
When moving an established local fleet to hosted mode, apply all repository migrations first and import the ignored account-scoped state file once. Worker schedule intent and incident history are included when present; legacy state documents receive an empty Worker record document.
cloudflare-fleet hosted import-state
The importer reads the D1 identifier from the ignored Wrangler configuration and refuses to replace populated hosted state. For populated stores, use the reviewed, history-preserving reconciliation workflow below instead of a force import.
Use one shared control plane from every device
Set CLOUDFLARE_FLEET_URL to the protected HTTPS origin and CLOUDFLARE_FLEET_ACCOUNT_ID to its expected account in each CLI or MCP process. D1 becomes authoritative for intent, activity, and Worker records; Cloudflare remains authoritative for live configuration, and the deployed Worker variables supply operator policy exceptions. The local state file is not a synchronized replica.
Load a dedicated Access service token through CLOUDFLARE_FLEET_ACCESS_CLIENT_ID and CLOUDFLARE_FLEET_ACCESS_CLIENT_SECRET. Add an application-specific Service Auth policy containing only that token, preserving the human login and MFA policies. Alternatively, provide an unexpired application JWT through CLOUDFLARE_FLEET_ACCESS_TOKEN. Do not combine credential methods. Follow Cloudflare's service-token instructions; keep values in a private secret manager and process environment, never arguments, URLs, or shared configuration.
Remote clients do not need or forward the Cloudflare account API token. The stdio MCP process connects through the same bounded hosted command API as the CLI; it does not expose a remote HTTP MCP endpoint. A configured hosted URL has no silent local fallback, and local file flags are rejected. cloudflare-fleet dashboard opens that URL; deployment policy controls writes.
export CLOUDFLARE_FLEET_URL="https://fleet.example.com"
export CLOUDFLARE_FLEET_ACCOUNT_ID="your-account-id"
cloudflare-fleet config show
cloudflare-fleet doctor --live
cloudflare-fleet mcp
Before cutover, stop writers and back up both stores. Apply all repository D1 migrations before deploying matching Worker and client versions. 0004_shared_control_state.sql adds account-scoped state archives, and the Worker diagnostics migration supplies the shared lease. The live doctor checks authenticated protocol, account binding, and required storage tables.
umask 077
CLOUDFLARE_FLEET_BACKEND=local cloudflare-fleet state export > local-backup.json
cloudflare-fleet state export > hosted-backup.json
jq '{state: ., intentSource: "incoming"}' local-backup.json > reconciliation.json
cloudflare-fleet state plan --input reconciliation.json --format json
cloudflare-fleet state apply --input reconciliation.json --expect-plan REVIEWED_DIGEST
Review the complete plan and choose incoming or hosted intent explicitly. Reconciliation preserves distinct activity and incident histories and rejects conflicting identities or pending operations. It rechecks revisions under exclusion, archives the prior state, atomically saves the merge, and verifies persistence. Inputs are bounded to 2 MiB and each apply to 500 additional activities. state export --archive-id ID reads a recovery copy; plan that copy as incoming to restore intent without deleting newer history. MCP provides get_fleet_state, plan_state_reconciliation, and apply_state_reconciliation with signed interactive approval for apply.
After cutover, keep standalone mode for deliberate offline inspection or recovery only. Do not alternate production writes between unsynchronized local and hosted stores. Revoke a lost device's Access token and issue a replacement limited to this application; a lost device does not require distributing the account API token.
Recover Worker diagnostics and schedule management
Run npm run db:migrate:remote before deploying a version that uses Worker diagnostics. 0002_worker_diagnostics.sql adds account-scoped intent and incident documents plus the Worker execution lease. Local state needs no manual migration: the optional workers section is created when intent or evidence is saved. Preserve it with the operation activity section in private backups.
Hosted browser, CLI, and MCP operations share an account-scoped write lease. An interrupted operation can leave pending activity, which blocks writes even after lease expiration. Stop old clients and independently inspect affected live resources, then use cloudflare-fleet recovery plan and recovery apply, or MCP plan_activity_recovery and apply_activity_recovery. Input requires activityId, an investigation reason, and stoppedClientsAndInspectedResources: true; apply requires the exact reviewed digest. Recovery retains the original plans as a failed execution with an explicitly unknown outcome, zero confirmed completions, and no automatic inverse. It does not retry, reverse, or verify Cloudflare writes. A verified inverse requires unchanged post-write schedules and deployment, not merely an available lease.
Online schedule edits do not change the owning Wrangler configuration. Record the configuration owner and reviewed reconciliation step in schedule intent, deliberately make that companion edit in its owning project, and review it again for rollback. Omitted Cron configuration preserves deployed schedules, while an explicit empty array removes them; Cloudflare documents both behavior and the propagation delay in Cron Triggers.
Use the dashboard's Workers panel, cloudflare-fleet worker --help, or the CLI and MCP workflow to inspect, review, apply, verify, and recover the same bounded operation. No recovery step implicitly redeploys code or changes routes, bindings, credentials, or databases.
Keep the local interfaces
Standalone local mode remains available, but it is a separate authority, not a replica of hosted D1. Select it explicitly when a hosted URL is configured. On macOS, provide the account token and identifier in the shell and launch the ordinary browser workflow. It shares the same UI, policy, intent schema, write planner, activity model, and verification code.
export CLOUDFLARE_FLEET_BACKEND=local
export CLOUDFLARE_API_TOKEN="your-account-token"
export CLOUDFLARE_ACCOUNT_ID="your-account-id"
cloudflare-fleet dashboard
Use cloudflare-fleet dashboard --write for the full local workflow. State defaults to $XDG_STATE_HOME/cloudflare-fleet/state.json, falling back to ~/.local/state/cloudflare-fleet/state.json; policy uses the corresponding per-user configuration directory. Select another profile with --state-file or --policy-file. The checkout-local ./launch.sh wrapper accepts the same dashboard options.
The local fleet CLI and stdio MCP server inherit the same credentials and use the same durable state profile:
cloudflare-fleet alignment list --format json
cloudflare-fleet mcp
An MCP client should use cloudflare-fleet as its command and ["mcp"] as its arguments. Keep the token in the inherited process environment, not a tracked or shared client configuration. The server opens no listener and exposes only named diagnostic, audit, intent, alignment, bounded change, activity, and guarded undo tools. The MCP setup guide includes copyable Codex and Claude Code configuration.
Keep the repository safe to publish
Operator state, deployment configuration, live screenshots, and browser traces stay outside version control. Validate the public source tree before proposing a change.
npm run check:publication
The publication check rejects private operator filenames, machine-local paths, unexpected symbolic links, malformed screenshots, and broken documentation links.
Before writes
Review the security model.
Understand the Access, Worker, proxy, persistence, and browser boundaries before enabling mutations.
Open security