Coolify Deployment
This guide walks you through deploying Orimora on Coolify — a self-hosted PaaS with built-in Traefik, TLS, and database management. No Kubernetes knowledge required.
Orimora runs as separate Coolify services (app, docs, database, Redis). Each can be updated independently.
orimora.com → orimora-app (SvelteKit — docker-compose.yaml)docs.orimora.com → orimora-docs (Starlight static site — /docs Dockerfile)Internal only (no public domain):
orimora-db → PostgreSQL 16orimora-redis → Redis 7Before you start
Section titled “Before you start”| Requirement | Notes |
|---|---|
| Coolify server | Docker-based, HTTPS via Let’s Encrypt |
| Domain names | e.g. orimora.com and docs.orimora.com → A records to server IP |
| GitHub access | Repo defcon1702/orimora, branch main (or your fork) |
Step-by-step setup
Section titled “Step-by-step setup”-
Create a Coolify project named e.g. “Orimora”, environment “production”.
-
Create PostgreSQL 16 inside the project (name:
orimora-db). After creation, Coolify shows an internal connection URL like:postgres://USER:PASSWORD@abc123def456:5432/orimoraCopy this — you need it for
DATABASE_URL. The hostname (abc123def456) is the database UUID, not a human-readable name. -
Create Redis 7 (
orimora-redis). Copy the internal URL, e.g.:redis://default:PASSWORD@xyz789:6379Use this for
REDIS_URL. -
Create the app service:
- Source: GitHub →
defcon1702/orimora, branchmain - Build pack: Docker Compose
- Docker Compose location:
/docker-compose.yaml(default — matches repo) - Do not set a base directory override
- Source: GitHub →
-
Assign domain — after Coolify parses the compose file, select the
orimoraservice and assignhttps://orimora.com(or your domain). -
Fill environment variables — Coolify auto-detects every
${VAR}from the compose file. Variables marked:?in the YAML appear as required (red border). See the tables below. -
Deploy the app — first deploy builds the Docker image, runs migrations on startup, and starts the server on port 3000.
-
Create the docs service (optional but recommended):
- Same repo and branch
- Build pack: Dockerfile
- Base directory:
/docs - Domain:
https://docs.orimora.com - Port:
80 - Env:
PUBLIC_SITE_URL=https://docs.orimora.com(if shown)
-
DNS — point both domains to your Coolify server IP. Wait for TLS certificates (automatic via Traefik).
-
Smoke test — open
https://orimora.com, complete onboarding, then:Terminal window curl -s https://orimora.com/api/health
Understanding docker-compose.yaml
Section titled “Understanding docker-compose.yaml”Coolify uses the file at the repository root. Here is what each section does — you do not need to edit this file in Coolify; configure everything via the UI env vars.
Service: orimora
Section titled “Service: orimora”services: orimora: build: context: . dockerfile: DockerfileBuilds the SvelteKit production image from the repo Dockerfile. Migrations run automatically in docker-entrypoint.sh before the server starts.
Required environment (:?) — 7 variables
These must be set in Coolify or the container fails at startup:
| Variable | What to enter | How to generate |
|---|---|---|
DATABASE_URL | Internal Postgres URL from step 2 | Copy from Coolify DB resource |
REDIS_URL | Internal Redis URL from step 3 | Copy from Coolify Redis resource |
SESSION_SECRET | 64 hex characters | node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" |
MAGIC_LINK_SECRET | 64 hex (different value) | Run the command again |
APP_URL | Public app URL | https://orimora.com — must match assigned domain |
LLM_ENCRYPTION_KEY | 64 hex | Run again — encrypts LLM API keys |
PUBLISHING_ENCRYPTION_KEY | 64 hex | Run again — encrypts webhook/publishing secrets |
App settings (optional defaults)
| Variable in YAML | Default | Meaning |
|---|---|---|
NODE_ENV | production | Fixed in compose — do not change |
PORT | 3000 | Internal container port (Traefik routes to this) |
ALLOW_REGISTRATION | false | Set true only during initial setup if needed |
COLLAB_SECRET | — | Optional shared secret for /collab WebSocket |
COLLAB_MAX_CONNECTIONS | 50 | Max simultaneous editors |
CRON_SECRET | — | Protects cleanup cron endpoint — generate a random string |
Reverse proxy / rate limiting (important on Coolify)
Section titled “Reverse proxy / rate limiting (important on Coolify)”- ADDRESS_HEADER=${ADDRESS_HEADER:-X-Forwarded-For}- XFF_DEPTH=${XFF_DEPTH:-1}Coolify terminates TLS at Traefik and forwards the real client IP via X-Forwarded-For. These defaults are correct for standard Coolify — leave them unless you add another proxy layer.
Reverse proxy / rate limiting
| Variable | Default | Why it matters |
|---|---|---|
ADDRESS_HEADER | X-Forwarded-For | Which header contains the client IP |
XFF_DEPTH | 1 | Which IP in the chain to trust (1 = immediate upstream) |
Without these, login rate limits collapse to one bucket for all users behind Traefik.
SMTP, OAuth, VAPID, S3, Backup
| Group | Key variables |
|---|---|
| SMTP | SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM |
| OAuth | GOOGLE_*, MICROSOFT_*, OIDC_* |
| VAPID | VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT |
| Upload storage (opt-in) | STORAGE_DRIVER, S3_BUCKET, S3_REGION, S3_ACCESS_KEY, S3_SECRET_KEY, S3_ENDPOINT, S3_FORCE_PATH_STYLE, S3_PRESIGN_TTL_SECONDS |
| Backup | BACKUP_ENABLED, BACKUP_PATH, BACKUP_RETENTION_*, BACKUP_SCHEDULE, BACKUP_S3_BUCKET |
Health check & graceful shutdown
Section titled “Health check & graceful shutdown”healthcheck: test: ['CMD', 'curl', '-f', 'http://localhost:3000/']stop_grace_period: 70sCoolify waits until the app responds on / before marking healthy. On redeploy, Docker waits up to 70 seconds for in-flight jobs (emails, webhooks) to finish.
Network
Section titled “Network” networks: - coolify
networks: coolify: external: trueThe app must join Coolify’s external coolify network to reach managed Postgres and Redis by UUID hostname. This is preconfigured — do not remove it.
Volume
Section titled “Volume”volumes: - orimora-uploads:/app/uploadsPersists uploaded files (image attachments, avatars, logos) across redeploys.
Volume ownership — image/avatar uploads fail with EACCES
Section titled “Volume ownership — image/avatar uploads fail with EACCES”The container runs non-root (USER node, UID 1000) and cannot chown a root-owned
volume at runtime. The image ships /app/uploads owned by node, so a fresh, empty named
volume inherits that ownership and uploads just work. But a volume created before this
directory existed in the image (older deploy), or one Coolify pre-created as root, stays
root-owned — then upload_image (MCP) and attachment uploads fail with
EACCES: permission denied, open '/app/uploads/attachments/…'.
Fix (one-time, from the Docker host or a Coolify terminal):
# Replace orimora-uploads with the actual volume name if Coolify prefixed it.docker run --rm -v orimora-uploads:/v alpine chown -R 1000:1000 /vThen redeploy. For a bind mount instead of a named volume, chown -R 1000:1000 the host
directory. Verify with docker run --rm -v orimora-uploads:/v alpine ls -lan /v — entries
should be owned by UID 1000.
Copy-paste checklist for Coolify env UI
Section titled “Copy-paste checklist for Coolify env UI”Minimum to get a running instance:
DATABASE_URL=postgres://USER:PASS@DB_UUID:5432/DATABASEREDIS_URL=redis://default:PASS@REDIS_UUID:6379APP_URL=https://orimora.comSESSION_SECRET=<64 hex>MAGIC_LINK_SECRET=<64 hex>LLM_ENCRYPTION_KEY=<64 hex>PUBLISHING_ENCRYPTION_KEY=<64 hex>ALLOW_REGISTRATION=falseCRON_SECRET=<random string>Recommended additions:
SMTP_HOST=smtp.example.comSMTP_PORT=587SMTP_USER=...SMTP_PASSWORD=...SMTP_FROM=noreply@orimora.comDOCS_URL=https://docs.orimora.comVariables not in docker-compose.yaml but used by the app (set in Coolify if needed): EXTRA_ALLOWED_ORIGINS, API_KEY_SECRET, webhook/publishing tuning — see Configuration.
Docs service (orimora-docs)
Section titled “Docs service (orimora-docs)”| Setting | Value |
|---|---|
| Build pack | Dockerfile |
| Base directory | /docs |
| Domain | https://docs.orimora.com |
| Port | 80 |
The app redirects /docs to DOCS_URL — set that env var on the app service to match.
Database migrations
Section titled “Database migrations”Migrations run automatically on every container start via run-migrations.mjs. No manual step for upgrades.
To debug inside the container terminal:
node run-migrations.mjsEach migration runs in its own transaction — a failure on migration 15 does not roll back 1–14.
First deploy workflow
Section titled “First deploy workflow”Correct first-deploy order:
- Create the Coolify service (Source: GitHub, Build pack: Docker Compose).
- Do not deploy yet. Go to the Environment Variables tab.
- Fill in all seven required variables (see table above) — at minimum
DATABASE_URL,REDIS_URL,APP_URL,SESSION_SECRET,MAGIC_LINK_SECRET,LLM_ENCRYPTION_KEY,PUBLISHING_ENCRYPTION_KEY. - Set
ALLOW_REGISTRATION=truetemporarily (first user setup). - Click Deploy. The build runs, migrations execute, the app starts.
- Complete onboarding at
https://orimora.comto create the admin user. - Set
ALLOW_REGISTRATION=falseagain and redeploy (or keep it per your policy).
If the container crashes on first deploy anyway, open Coolify → service → Logs and look for Missing required environment variable. The error names the exact variable.
Upgrading Orimora
Section titled “Upgrading Orimora”Migrations run automatically on container start — no manual yarn db:migrate needed. The upgrade workflow is:
- In Coolify, click Redeploy (or push a new commit to
mainif the webhook is configured). - Coolify pulls the latest image, runs the new container alongside the old one (rolling update).
docker-entrypoint.shcallsrun-migrations.mjsbefore the server starts — new schema is applied.- Old container is removed once the health check passes on the new one.
Rollback: If a migration causes issues, use Coolify → service → Deployments to roll back to the previous image. Note that schema rollbacks require a manual SQL revert — migrations are not automatically reversible. Always snapshot the database (Coolify database backup or pg_dump) before upgrading major versions.
Known pitfalls
Section titled “Known pitfalls”| Problem | Cause | Fix |
|---|---|---|
| Container crashes immediately | Required :? var missing (see First deploy) | Fill all seven required vars before first deploy |
| Duplicate empty env vars | Coolify auto-creates vars from ${VAR} and you added them manually | Delete empty duplicates in Coolify UI |
EAI_AGAIN / DB timeout | App not on coolify network or wrong hostname | Use internal URL from Coolify DB resource; hostname is UUID |
| DB hostname changed | Recreated Postgres resource | Update DATABASE_URL with new UUID |
| Boot crash, no clear error | Missing :? required var | Check all seven required vars above |
| Rate limit affects everyone | XFF_DEPTH wrong | Keep default 1 for standard Coolify |
| Uploads lost on redeploy | Volume not mounted | Do not remove orimora-uploads volume |
Image upload fails (EACCES) | orimora-uploads volume is root-owned (not UID 1000) | docker run --rm -v orimora-uploads:/v alpine chown -R 1000:1000 /v (details) |
| Webhook not triggering on docs push | Coolify builds on any push to main | Set watch_paths: docs/** in Coolify to scope rebuilds |
Maintenance mode (Basic Auth)
Section titled “Maintenance mode (Basic Auth)”Coolify → app service → General → HTTP Basic Auth → enable, set credentials, redeploy. Blocks all access at Traefik until you disable it.
Registration gate
Section titled “Registration gate”ALLOW_REGISTRATION=false (default) blocks open sign-up via magic link or OAuth. The first user (onboarding) and invited users can still join.
DNS reference
Section titled “DNS reference”| Record | Type | Value |
|---|---|---|
orimora.com | A | Server IP |
docs.orimora.com | A | Server IP |
Wildcard *.orimora.com simplifies future subdomains.
Further reading
Section titled “Further reading”- Installation — local development
- Configuration — full env reference including vars not in compose
- Database Migrations — how the runner works
- Internal ops reference:
deploy/coolify-traefik.md