This guide covers deploying Roomote on Coolify, the
self-hosted PaaS, as a Docker Compose resource. The maintained service
specification lives in docker-compose.yaml; you
paste it into Coolify when creating the resource, and it is the source of
truth that maintainers keep up to date.
For single-host Docker deployments without Coolify, use the one-command installer or the Compose paths in SELF_HOSTING.md instead. For a managed PaaS with no server of your own, see deploy/railway.
- Coolify's proxy is the HTTPS edge. There is no bundled Caddy and no
/_roomote-apipath routing. The web app, the API, and MinIO each get their own public domain throughSERVICE_FQDN_*declarations in the compose file, soTRPC_URLpoints at the api service's domain directly. GitHub webhooks and sandbox workers call that API origin, and Slack webhooks arrive at the web origin and are proxied to the API internally. - The Docker socket is available. Unlike Railway, the Coolify host runs
Docker, so the
dockersandbox provider works and is the template default: the controller mounts/var/run/docker.sockand runs task workers on the same server. Hosted sandboxes (Modal, E2B, or Daytona) are a config-only swap; see Task execution. - No openssl provisioning step. The template sets
R_AUTO_GENERATE_KEYS=true, so Roomote generates theJOB_AUTH_*/PREVIEW_AUTH_*P-256 keypairs at first boot and persists them encrypted (withENCRYPTION_KEY) in Postgres. Every other secret is generated by Coolify's magic environment variables (SERVICE_PASSWORD_*,SERVICE_BASE64_*). - Shared secrets are defined once. Coolify resolves the same magic
variable name to the same generated value across every service in the
resource, so the compose file can reference
SERVICE_PASSWORD_MINIOfrom both MinIO and the app services. There is no per-service secret anchor like Railway's${{api.NAME}}indirection. - The setup wizard is token-gated by default. Coolify domains are
publicly reachable, so the template generates
SETUP_TOKEN(SERVICE_PASSWORD_SETUPTOKEN). Copy it from the resource's environment variables panel to open/setup. - Live previews are off by default. Preview subdomains need a wildcard domain routed to the optional preview-proxy service; everything else works without them (see below).
- A server running Coolify with enough headroom for the stack. With the
default
dockersandbox provider, task workers run on the same host — plan for 8 GB+ RAM. With hosted sandboxes, 4 GB+ is enough for the control plane. - DNS you control for the web, api, and minio domains, or Coolify's
generated domains for a quick trial. Set all three to
https://— GitHub App creation and webhooks require HTTPS origins, and Coolify's proxy issues Let's Encrypt certificates forhttps://domains automatically. - A model provider API key, for example OpenRouter (entered in the setup wizard, not at deploy time). When using hosted sandboxes instead of the Docker socket, also a Modal, E2B, or Daytona account.
The published app and worker images (ghcr.io/roocodeinc/roomote-app and
ghcr.io/roocodeinc/roomote-worker) are public and pulled anonymously; no
registry login is needed.
The template tracks the mutable :develop alias, which the publish
workflow moves to every new develop build (releases also move :latest).
Nothing else in the template encodes a version: the images bake
RELEASE_VERSION, the app derives DOCKER_WORKER_IMAGE from it when unset,
and the controller reads the worker release version from the VERSION file
inside worker-current.tar.gz.
To pin instead (recommended for production deployments): change the
x-roomote-app-image anchor at the top of the compose file to an immutable
tag (v* or develop-<sha>). No other edits are needed — the derived values
follow the image.
- In Coolify, open (or create) a project and click + New → Docker Compose Empty.
- Paste the contents of
docker-compose.yamland save. Coolify parses the file, lists the services, and generates values for everySERVICE_*magic variable. - In the service settings, set the domains: the web service's domain is
the app origin users open, the api service's domain is what GitHub
webhooks and workers call, and the minio service's domain serves
presigned artifact URLs (routed to port 9000). Use
https://for all three. Coolify generates placeholder domains when the server has a wildcard domain configured; replace them with real ones for production. - Deploy. The
db-migrateone-shot runs schema migrations before the app services start serving (it exits after each run — that is expected; theexclude_from_hcfield keeps it from affecting resource health).
| Service | Image | Public domain | Healthcheck |
|---|---|---|---|
postgres |
pinned postgres:17.5 |
no | pg_isready |
redis |
pinned redis:7-alpine |
no | redis-cli ping |
minio |
pinned MinIO + volume | yes (routed to port 9000) | mc ready local |
db-migrate |
roomote-app:develop |
no (one-shot) | excluded |
web |
roomote-app:develop |
yes (port 3000) | /health |
api |
roomote-app:develop |
yes (port 3001) | /health/liveness |
controller |
roomote-app:develop |
no | /health/controller via API |
bullmq |
roomote-app:develop |
no | /admin/health |
preview-proxy |
roomote-app:develop |
optional (wildcard domain) | /health |
The shared env block is defined once as the x-roomote-shared-env YAML
anchor and merged into every app service. The generated values live on the
resource's environment variables panel in Coolify, where you can read
SETUP_TOKEN and DASHBOARD_PASSWORD after the first deploy.
Notes:
- Leave
DOCKER_WORKER_IMAGEunset. The app derives it from theRELEASE_VERSIONbaked into the running image, so it always matches the deployed build. Setting it explicitly silently pins the worker to whatever version the value encodes. - Sandbox provider credentials (Modal token pair when using Modal) and
model provider keys such as
OPENROUTER_API_KEYare not template variables. Enter them in the/setupwizard (or Settings → Sandboxes / Models) after first boot; they are stored encrypted in Postgres. - Leave
JOB_AUTH_*andPREVIEW_AUTH_*unset —R_AUTO_GENERATE_KEYS=truemanages them. If you later provide explicit env values, they take precedence over the persisted keypairs. - Leave
PREVIEW_PROXY_BASE_URLandPREVIEW_DOMAINSunset unless you enable live previews. Roomote boots without them; previews report as not configured in Settings → Live Previews until set.
The template defaults to DEFAULT_COMPUTE_PROVIDER=docker: the controller
mounts the host Docker socket and starts one worker container per task on
the same server. Two things matter in this mode:
-
Worker network. The controller uses the network named by
DOCKER_WORKER_NETWORK(roomote_default, declared at the bottom of the compose file) to discover the labeled API and optional preview-proxy containers. It creates a separate bridge network for each task and attaches only the API plus the preview proxy when enabled, so workers do not joinroomote_default, see sibling tasks, or reach Postgres, Redis, and MinIO. If worker containers fail to start after a deploy, confirm the configured discovery network exists withdocker network lsand that Coolify preserved the standard Compose service labels.Docker task writable layers fail closed when the host storage driver cannot enforce
DOCKER_WORKER_DISK_LIMIT. KeepDOCKER_WORKER_ALLOW_UNBOUNDED_DISK=falseunless the host already enforces an equivalent quota outside Docker. -
Trust boundary. Mounting the Docker socket gives the controller control of the host Docker daemon. This matches the single-host production shape and is intended for a trusted, single-tenant server.
To use hosted sandboxes instead (no socket access, task execution bills through the provider):
- Set
DEFAULT_COMPUTE_PROVIDER=modal(ore2b/daytona) and addEXCLUDED_COMPUTE_PROVIDERS=dockerto the shared env block. - Remove the
/var/run/docker.sockvolume from the controller service. - Keep
DOCKER_WORKER_RELEASE_PATH— the controller uploads the baked-in worker release archive into hosted sandboxes at spawn time. - Enter the provider credentials in the
/setupwizard after first boot.
MinIO does not auto-create buckets, so the template sets
S3_AUTO_CREATE_BUCKET=true: at boot, the api creates the artifacts bucket
when it is missing. Watch for [artifacts-bucket] Created S3 bucket ... in
api's logs after the first deploy.
Presigned artifact URLs use the minio service's public domain
(S3_PRESIGN_ENDPOINT), so workers and browsers can download artifacts
without reaching into the compose network. Alternatively, skip bundled MinIO
and point the S3 values at an external S3-compatible store (AWS S3 or
Cloudflare R2, with S3_REGION=auto for R2). Roomote uses path-style
addressing, and S3_PRESIGN_ENDPOINT must be reachable from workers.
- Wait for
webandapito report healthy. On a brand-new resource the first boot also generates and persists the auth keypairs (watch for[auth-keypairs] Generated ...in an app service's logs). - Copy
SETUP_TOKENfrom the resource's environment variables panel and openhttps://<web-domain>/setup?token=<SETUP_TOKEN>. - Create the founding admin account (email/password works immediately; Slack or Microsoft sign-in can be added later).
- Connect GitHub with Create GitHub App — the manifest flow derives the
callback and webhook URLs from
R_APP_URLandTRPC_URL, so no manual URL entry is needed. - Enter the model provider key when the wizard asks. With the default Docker provider there are no sandbox credentials to enter; with hosted sandboxes, enter the provider tokens and let the wizard build the E2B template or Daytona snapshot when applicable.
- Pick repositories, create an environment, and run a small task end to end (the SELF_HOSTING.md verification checklist applies from step 2 onward).
Live previews need a wildcard domain, which requires DNS you control and a wildcard-capable TLS setup on the Coolify proxy:
- Uncomment the
preview-proxyservice in the compose file and setPREVIEW_PROXY_BASE_URL,NEXT_PUBLIC_PREVIEW_PROXY_BASE_URL, andPREVIEW_DOMAINSto your preview domain. TheNEXT_PUBLIC_variant must also be set on thewebservice — the web client builds preview links from it. - Point
previews.<your-domain>and*.previews.<your-domain>at the Coolify server and route them to the preview-proxy service. Wildcard HTTPS certificates require a DNS-01 challenge in Coolify's proxy configuration (see Coolify's wildcard/custom-proxy docs); this is the advanced part and the reason previews stay off by default. - Opt in from Settings → Live Previews, which validates the wildcard hostname and enables previews per deployment and environment.
- Upgrade a deployment on
:developby redeploying the resource — Coolify pulls the current alias,db-migrateapplies any schema changes before the app services restart, and the auto-generated keypairs persist in Postgres, so sessions, job tokens, and preview tokens survive redeploys. On an immutable pin, bump the tag in thex-roomote-app-imageanchor first. - Back up the Postgres volume (or run
pg_dumpagainst thepostgrescontainer) and the MinIO volume. Everything else is reproducible from config plus the generated environment values on the resource. - Costs are your server. With the default Docker provider everything — control plane and task execution — runs on the Coolify host. With hosted sandboxes, task execution bills through the provider and model usage bills through your model provider.
When the template needs to change, edit
docker-compose.yaml and re-run the first-boot
verification on a scratch Coolify resource before merging. Keep the file
paste-ready: Coolify-specific fields (SERVICE_* magic variables,
exclude_from_hc) are intentional and are consumed by Coolify's compose
parser, not by plain docker compose.