Deploy with Cloudflare Containers

Cloudflare Containers are generally available and can run Doso's existing Linux/amd64 Rust image. A Worker provides TLS, routing, and lifecycle control; the API container connects to managed Postgres over TLS.

Start with one API instance selected by a stable ID. Cloudflare currently requires application-defined container routing; built-in autoscaling is not available. Random routing is unsafe for Doso until run ownership, settings invalidation, and event delivery are distributed.

Prerequisites#

  • Workers Paid plan with Containers enabled.
  • Node.js and Wrangler.
  • Docker capable of building linux/amd64.
  • Managed Postgres reachable from Cloudflare with TLS.
  • A stable 32-byte settings encryption key.
  • A Node-compatible home for web: either its own Cloudflare Container or an external Next.js origin.

Worker package#

This folder contains a deployable Worker:

  • wrangler.jsonc declares the API Container, Durable Object binding, one-instance ceiling, and observability.
  • src/index.ts forwards /v1/** to the stable doso-api-primary container and proxies other traffic to WEB_ORIGIN.
  • package.json provides development, type-check, and deploy scripts.

Install and configure from this folder:

1npm install2npx wrangler secret put DATABASE_URL3npx wrangler secret put DOSO_SETTINGS_KEY_HEX4npx wrangler secret put BETTER_AUTH_SECRET5npm run deploy

The API Dockerfile needs the repository root as its build context. Build and push the image first, then replace YOUR_ACCOUNT_ID in wrangler.jsonc:

1docker build --platform linux/amd64 \2  -f ../../../crates/doso-api/Dockerfile \3  -t registry.cloudflare.com/YOUR_ACCOUNT_ID/doso-api:latest ../../..4npx wrangler containers push \5  registry.cloudflare.com/YOUR_ACCOUNT_ID/doso-api:latest

Environment and secrets#

Set public values in wrangler.jsonc and secrets with Wrangler:

1DATABASE_URL=postgresql://.../doso?sslmode=require2DOSO_SETTINGS_KEY_HEX=<64 hex characters>3DOSO_AUTH_MODE=jwks4DOSO_JWKS_URL=https://app.example.com/api/auth/jwks5DOSO_AUTH_ISSUER=https://app.example.com6DOSO_AUTH_AUDIENCE=https://app.example.com7DOSO_PUBLIC_API_URL=https://app.example.com8DOSO_POSTGRES_MAX_CONNECTIONS=39DOSO_HOSTED_RUNTIME=true

The Container class passes these values into the process on each start. Keep general internet egress enabled: Cloudflare outbound handlers only intercept HTTP/HTTPS, so enableInternet=false also blocks direct Postgres on port 5432. Restrict database credentials and require TLS at the database instead.

Scaling and lifecycle#

The example uses:

  • max_instances: 1;
  • stable container ID doso-api-primary;
  • port 8080;
  • sleepAfter: "30m";
  • request/response streaming without buffering.

An SSE connection can remain open, and the client reconnects if a rollout or sleep/wake cycle interrupts it. Reconnecting cannot recover process-local events that were never persisted, so the UI should reconcile status from Postgres after reconnect.

Before adding replicas, implement:

  1. run-owner leases and heartbeats instead of global startup stale-run cleanup;
  2. durable queues for ingestion and workflow work that outlives a request;
  3. shared pub/sub or database-backed event cursors;
  4. cross-process settings invalidation;
  5. a one-shot migration/release job.

Cloudflare databases and storage#

Cloudflare offers several data products, but they are not interchangeable:

  • D1 is Cloudflare's serverless SQLite database, accessed through a Worker binding. The Axum/SQLx container cannot open a D1 connection. Using D1 for Doso's primary graph store requires a new store implementation, transaction model, migration path, and likely per-tenant sharding.
  • Hyperdrive pools and accelerates connections from Workers to external Postgres/MySQL. Its connection string exists inside a Worker binding; it is not a Postgres endpoint the container can consume.
  • R2 is appropriate for uploads, immutable artifacts, exports, and backups. It is not a replacement for Postgres or the settings encryption key. A future object-store abstraction can use its S3-compatible API.
  • Durable Object SQLite is useful for routing or coordination state owned by the Worker. It does not replace Doso's relational store.

A narrow future D1 candidate is the tenant registry/audit control plane. Keep the primary graph and workflow data in Postgres.

Web and SaaS#

For the simplest first deployment, leave web on a Node host and set WEB_ORIGIN to its HTTPS URL. To keep all compute on Cloudflare, deploy web/Dockerfile as a second deterministic Container and route non-/v1 traffic to it; use external Postgres for Better Auth.

Multi-org is ../../saas, not a third container on this page.

Verify and operate#

  1. Confirm the API image reports healthy on port 8080.
  2. Test JWKS retrieval and an authenticated /v1/** request.
  3. Test upload limits and SSE through the Worker without buffering.
  4. Let the container sleep, then verify wake-up and settings decryption.
  5. Deploy a new image while streaming and verify reconnect/reconciliation.
  6. Inspect Worker, Durable Object, and Container logs separately.

Official references: Container scaling, environment variables, outbound traffic, D1, and Hyperdrive.