Self-hosting

Reverse proxy & TLS

Put HTTPS in front of a self-hosted installation with Caddy or nginx — hostnames, WebSockets for Convex, upload sizes, timeouts and security headers.

On this page

The quickstart stack publishes its ports on 127.0.0.1 (DESYN_BIND). A reverse proxy on the host terminates TLS and forwards each hostname to its port:

HostnameUpstreamNotes
design.example.org127.0.0.1:3000Web app
convex.example.org127.0.0.1:3210Convex API; WebSockets required
convex-site.example.org127.0.0.1:3211HTTP actions: auth, Agent API, MCP (long-lived streaming responses)
s3.example.org127.0.0.1:9000MinIO S3 API; large uploads
docs.example.org (optional)127.0.0.1:3002This documentation, with the marketing profile

Do not publish the worker (it has no port), the Convex dashboard or the MinIO console (reach them over an SSH tunnel). After the proxy works, set the public URLs as described in choose your URLs and rebuild the web image.

Caddy obtains and renews certificates automatically and proxies WebSockets without extra configuration.

/etc/caddy/Caddyfile
design.example.org {
	encode zstd gzip
	reverse_proxy 127.0.0.1:3000 {
		header_up X-Real-IP {remote_host}
	}
}

convex.example.org {
	reverse_proxy 127.0.0.1:3210
}

convex-site.example.org {
	reverse_proxy 127.0.0.1:3211 {
		header_up X-Real-IP {remote_host}
		flush_interval -1
	}
}

s3.example.org {
	request_body {
		max_size 1GB
	}
	reverse_proxy 127.0.0.1:9000
}

Reload with sudo systemctl reload caddy.

WebSockets#

The editor keeps a WebSocket open to the Convex API for live updates. If the proxy does not forward Upgrade headers, or closes idle connections after a short timeout, the app loads but stays in "connecting…" or keeps reconnecting. Caddy handles this automatically; in nginx use the Upgrade/Connection headers and long read timeouts shown above.

Upload size#

Uploads go straight to the storage hostname. Its body size limit must be at least the largest file you allow (1 GB by default for self-hosted). nginx's default of 1 MB will make most uploads fail.

Client IP addresses#

Sign-in and other per-IP rate limits identify clients by the X-Real-IP header first, then the first X-Forwarded-For entry. Clients can send these headers themselves, so the proxy must overwrite X-Real-IP with the real peer address on the app and HTTP actions hostnames, as both configurations above do. Otherwise anyone can bypass the limits by sending a different address with every request.

If your proxy sits behind another layer, such as Cloudflare, list the headers to trust in the backend's AUTH_IP_HEADERS (for example cf-connecting-ip,x-real-ip), and only headers that layer sets itself. See the configuration reference.

Streaming responses#

The Agent API and MCP endpoints on the HTTP actions hostname can stream for minutes during long agent runs. Disable response buffering there and allow long read timeouts.

Security headers#

Add HSTS (Strict-Transport-Security: max-age=31536000) once HTTPS works for all hostnames. The web app sets its own content security policy; if you add one at the proxy, allow connect-src to your Convex API (including wss://), HTTP actions and storage hostnames.

Serving the docs#

The stack's docs service (profile marketing) serves this documentation on its own hostname: route docs.example.org to 127.0.0.1:3002 and set DESYN_DOCS_URL=https://docs.example.org before building it. Its image is built for the root path. To serve the docs under a path of another site instead, such as /docs, build apps/docs with DOCS_BASE_PATH=/docs (see the app's README) and route /docs and /docs/* to it with the path unchanged.

Popular pages

QuickstartGet started
Keyboard shortcutsEditor
The agentAI
Docker Compose quickstartSelf-hosting
Configuration referenceSelf-hosting
Agent API (v1)Developers