Architecture
The services in the Compose stack, how traffic reaches them and how the backend routes requests by hostname.
Request flow
Everything public enters through the proxy on ports 80 and 443. The databases are reachable only inside the Compose network.
flowchart TD U[Apps, browsers, SDKs]:::evt --> P[Caddy proxy]:::proc P -->|dashboard host| D[Dashboard]:::proc P -->|api, sdk, link hosts| W[Rails web]:::proc D -->|API calls| P W --> PG[(PostgreSQL)]:::done W --> R[(Redis)]:::done W --> CH[(ClickHouse)]:::done W --> S[(Uploads volume)]:::done R -->|jobs and events| K[Sidekiq workers]:::proc K --> PG K --> CH
Behind your own proxy (Coolify, Dokploy and other platforms) the proxy service is left out and your proxy routes the same hosts to dashboard:3000 and web:3000.
Services
| Service | Role | Port (internal) | Volume | Scaling |
|---|---|---|---|---|
proxy | Caddy with automatic TLS. standalone profile only. | 80, 443, published | caddy_data, caddy_config | One per server |
postgres | PostgreSQL 16. The system of record. | 5432 | pg_data | Larger server |
redis | Redis 7. Queues, caches, dedup. AOF, no eviction. | 6379 | redis_data | Larger server |
clickhouse | ClickHouse 25.3. Events and analytics. | 8123 | clickhouse_data | Larger server |
migrate | Migrates both databases, seeds the OAuth app and admin | None | storage | Runs on every start, then exits |
web | Rails (Puma): API, redirects, SDK, uploads | 3000 | storage | WEB_CONCURRENCY |
worker-1 | Sidekiq: scheduler, events, batches | None | storage | Never. Exactly one replica. |
worker-2 | Sidekiq: maintenance and device updates | None | storage | Larger server |
dashboard | Next.js dashboard, configured from .env | 3000 | None | Stateless |
Start order is fixed: the three databases become healthy, migrate exits with code 0, web becomes healthy, then the workers and dashboard start. Sizing covers the process settings, and Monitoring lists each healthcheck.
Uploaded images and exports live in the storage volume and are served through the API host. To use S3 or an S3-compatible store instead, set ACTIVE_STORAGE_SERVICE=amazon and the AWS_S3_* variables. Railway, Render, Easypanel and CapRover use object storage so the API and workers share the same uploads.
Hostname routing
The backend routes by hostname. Fixed hosts live under your app domain (SERVER_HOST). Links live under your links domain (DOMAIN_LIVE) and test links domain (DOMAIN_TEST). These default to the app domain and test.<links domain>.
| Host | Serves |
|---|---|
dashboard. | Dashboard UI |
api. | Dashboard API, uploaded assets, health check |
sdk. | Mobile, web and server SDK endpoints. This is the SDK baseURL. |
preview. | Link preview pages |
mcp. | MCP (AI tool) OAuth and API |
go. | Reserved for the quick-link helper, which is not enabled on self-hosted deployments |
*.<links domain> | Per-project production link subdomains, for example a1b2c3d4.acme.link |
*.<test links domain> | Per-project test link subdomains, for example a1b2c3d4.test.acme.link |
DNS lists the records each host needs.