Self-hosting Convex
Run Convoy fully on your own infrastructure with the bundled Docker Compose setup — walkthrough, environment matrix, and what you give up versus managed Convex.
Convoy's backend is Convex, and Convex ships an official self-hosted
build. The repo bundles a Docker Compose setup under self-hosted/ that
runs the whole stack on one machine:
backend— self-hosted Convex (ghcr.io/get-convex/convex-backend); database, functions, scheduler, file storage. Client API on port3210, HTTP actions (/api/mcp,/api/agent/*) on port3211.dashboard— the Convex dashboard (ghcr.io/get-convex/convex-dashboard) on port6791; data browser, logs, function runner.web— the Convoy Next.js app, built locally from your checkout, on port3000.
What still lives in the cloud
There is no fully offline, single-image Convoy. Two things cannot be bundled:
- Clerk (required). All authentication goes through Clerk, which has no
self-hosted offering. A free-tier Clerk app works fine for a local
deployment; the login flow needs internet access to
*.clerk.accounts.dev. - Resend (optional). Invitation and notification emails go through Resend. Skip it and everything else works — invitations can be accepted via link instead of email.
A single pull-and-run image is also ruled out by Next.js itself: the
NEXT_PUBLIC_* values (Convex URL, Clerk publishable key) are baked into
the JavaScript bundle at build time, so the web image must be built per
installation. docker compose build handles that.
Quick start
Prerequisites: Docker with Compose, and a Clerk application set up per
Auth and Clerk (create the convex JWT
template).
./self-hosted/start.shThe script asks for your two Clerk API keys on the first run and does the
rest: writes .env, generates the instance secret and admin key, derives
the Clerk JWT issuer domain from the publishable key, starts the backend
and dashboard, builds the web image, deploys Convoy's functions from
inside Docker (no Node.js needed on the host), and starts the web app.
Re-running it is always safe.
Manual walkthrough
The same steps by hand — useful for understanding the moving pieces or for
customized setups. Requires Node 20+ on the host for the
npx convex deploy step.
cd self-hosted
cp .env.example .envFill in .env: INSTANCE_SECRET (openssl rand -hex 32),
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, and CLERK_SECRET_KEY.
1. Start the Convex backend and dashboard:
docker compose up -d backend dashboard2. Generate an admin key (the self-hosted equivalent of a deploy key)
and paste it into .env as CONVEX_SELF_HOSTED_ADMIN_KEY:
docker compose exec backend ./generate_admin_key.sh3. Deploy Convoy's functions and schema. The standard Convex CLI
targets a self-hosted backend when CONVEX_SELF_HOSTED_URL and
CONVEX_SELF_HOSTED_ADMIN_KEY are set:
cd ../apps/web
set -a; source ../../self-hosted/.env; set +a
# If apps/web/.env.local exists (dev checkout), its CONVEX_DEPLOYMENT
# conflicts with self-hosted deploys — clear it for these commands:
export CONVEX_DEPLOYMENT=
npx convex env set CLERK_JWT_ISSUER_DOMAIN "https://<your-clerk-issuer-domain>"
# Optional, for invitation emails:
# npx convex env set RESEND_API_KEY "re_..."
# npx convex env set EMAIL_FROM "Convoy <noreply@yourdomain>"
npx convex deploy4. Build and start the web app:
cd ../../self-hosted
docker compose up -d --build webOpen http://127.0.0.1:3000, sign in through Clerk, and create your first
organization. The Convex dashboard is at http://127.0.0.1:6791 (log in
with the admin key).
5. Connect the CLI. Nothing changes for CLI users — convoy setup
against http://127.0.0.1:3000 works as usual. The CLI derives the HTTP
actions endpoint from the Convex URL: *.convex.cloud → *.convex.site
on managed Convex, port :3210 → :3211 on self-hosted. Keep that port
convention if you put a reverse proxy in front.
Serving beyond localhost
The defaults assume one machine. To serve a team:
- Set
CONVEX_CLOUD_ORIGIN,CONVEX_SITE_ORIGIN, andAPP_URLin.envto URLs reachable from users' browsers and CLI machines (these origins are embedded in tokens and the web bundle — they must match exactly). - Put TLS in front (Caddy/nginx/Traefik). Preserve the
:3210/:3211pairing, e.g.convex.example.com:3210andconvex.example.com:3211. - Rebuild the web image after changing
.env(docker compose up -d --build web) — the values are baked in at build.
Operations
- Upgrades:
docker compose pull && docker compose up -d, then re-runnpx convex deployafter pulling a new Convoy version. Pin image tags instead oflatestfor reproducibility, and follow convex-backend releases. - Backups: data lives in the
convex_dataDocker volume (embedded SQLite + file storage). Snapshot the volume, or usenpx convex exportfor a logical export. You own backups now — there is no managed restore. - Scaling: the self-hosted image is a single-node build. Vertical
scaling only; for real load you can move the database to Postgres/MySQL
(
DATABASE_URL) and file storage to S3, but the backend itself stays one process. - Optional backend flags:
docker-compose.ymlships with a few commented-out variables. The backend rejects empty values at boot, so each one must be uncommented in the compose file and given a value in.env:DATABASE_URL(external Postgres/MySQL instead of the embedded SQLite volume),DO_NOT_REQUIRE_SSL=true(when that database connection has no TLS — typical for a same-host Postgres), andREDACT_LOGS_TO_CLIENT=true(hide server error details from connected clients — recommended once real users are on the instance).
What you lose versus managed Convex
Self-hosted Convex has feature parity with the cloud free tier for the
things Convoy uses — realtime queries, mutations, actions, HTTP actions,
scheduler/crons, file storage, full-text search, and components (Convoy
uses migrations and resend) all work. What you give up is the managed
layer around it:
| Capability | Managed Convex | Self-hosted |
|---|---|---|
| Realtime, scheduler, file storage, components | ✓ | ✓ |
| Scaling | Automatic, multi-service | Single node, vertical only |
| Backups / restore | Managed snapshots, point-in-time | Your volume snapshots + npx convex export |
| Log streaming (Axiom, Datadog, …) | ✓ | Not sent; use docker logs / dashboard |
| Preview & per-developer cloud deployments | ✓ | One deployment; run more compose stacks yourself |
| Dashboard access control | Team accounts | Single shared admin key |
| Availability, upgrades, on-call | Convex's problem | Your problem |
None of these remove Convoy features. Everything in the product — tasks, realtime updates, attachments, invitations (with Resend), MCP server, CLI agent threads — runs on self-hosted Convex. The differences are operational.
Validation status
This recipe is new. The self-hosted/ compose file follows the
official Convex self-hosting guide
and Convoy's URL handling supports the self-hosted port convention, but
if you hit issues the most likely suspects are the ones any self-host
deployer should verify first: Clerk JWT validation from inside your
network, file uploads, scheduled jobs (task hard-delete, stream
watchdogs), and CLI agent registration over :3211.