Skip to content

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 16
orimora-redis → Redis 7

RequirementNotes
Coolify serverDocker-based, HTTPS via Let’s Encrypt
Domain namese.g. orimora.com and docs.orimora.com → A records to server IP
GitHub accessRepo defcon1702/orimora, branch main (or your fork)

  1. Create a Coolify project named e.g. “Orimora”, environment “production”.

  2. Create PostgreSQL 16 inside the project (name: orimora-db). After creation, Coolify shows an internal connection URL like:

    postgres://USER:PASSWORD@abc123def456:5432/orimora

    Copy this — you need it for DATABASE_URL. The hostname (abc123def456) is the database UUID, not a human-readable name.

  3. Create Redis 7 (orimora-redis). Copy the internal URL, e.g.:

    redis://default:PASSWORD@xyz789:6379

    Use this for REDIS_URL.

  4. Create the app service:

    • Source: GitHub → defcon1702/orimora, branch main
    • Build pack: Docker Compose
    • Docker Compose location: /docker-compose.yaml (default — matches repo)
    • Do not set a base directory override
  5. Assign domain — after Coolify parses the compose file, select the orimora service and assign https://orimora.com (or your domain).

  6. 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.

  7. Deploy the app — first deploy builds the Docker image, runs migrations on startup, and starts the server on port 3000.

  8. 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)
  9. DNS — point both domains to your Coolify server IP. Wait for TLS certificates (automatic via Traefik).

  10. Smoke test — open https://orimora.com, complete onboarding, then:

    Terminal window
    curl -s https://orimora.com/api/health

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.

services:
orimora:
build:
context: .
dockerfile: Dockerfile

Builds 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:

VariableWhat to enterHow to generate
DATABASE_URLInternal Postgres URL from step 2Copy from Coolify DB resource
REDIS_URLInternal Redis URL from step 3Copy from Coolify Redis resource
SESSION_SECRET64 hex charactersnode -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
MAGIC_LINK_SECRET64 hex (different value)Run the command again
APP_URLPublic app URLhttps://orimora.commust match assigned domain
LLM_ENCRYPTION_KEY64 hexRun again — encrypts LLM API keys
PUBLISHING_ENCRYPTION_KEY64 hexRun again — encrypts webhook/publishing secrets
App settings (optional defaults)
Variable in YAMLDefaultMeaning
NODE_ENVproductionFixed in compose — do not change
PORT3000Internal container port (Traefik routes to this)
ALLOW_REGISTRATIONfalseSet true only during initial setup if needed
COLLAB_SECRETOptional shared secret for /collab WebSocket
COLLAB_MAX_CONNECTIONS50Max simultaneous editors
CRON_SECRETProtects 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
VariableDefaultWhy it matters
ADDRESS_HEADERX-Forwarded-ForWhich header contains the client IP
XFF_DEPTH1Which 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
GroupKey variables
SMTPSMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM
OAuthGOOGLE_*, MICROSOFT_*, OIDC_*
VAPIDVAPID_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
BackupBACKUP_ENABLED, BACKUP_PATH, BACKUP_RETENTION_*, BACKUP_SCHEDULE, BACKUP_S3_BUCKET
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:3000/']
stop_grace_period: 70s

Coolify 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.

networks:
- coolify
networks:
coolify:
external: true

The app must join Coolify’s external coolify network to reach managed Postgres and Redis by UUID hostname. This is preconfigured — do not remove it.

volumes:
- orimora-uploads:/app/uploads

Persists 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):

Terminal window
# Replace orimora-uploads with the actual volume name if Coolify prefixed it.
docker run --rm -v orimora-uploads:/v alpine chown -R 1000:1000 /v

Then 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.


Minimum to get a running instance:

Terminal window
DATABASE_URL=postgres://USER:PASS@DB_UUID:5432/DATABASE
REDIS_URL=redis://default:PASS@REDIS_UUID:6379
APP_URL=https://orimora.com
SESSION_SECRET=<64 hex>
MAGIC_LINK_SECRET=<64 hex>
LLM_ENCRYPTION_KEY=<64 hex>
PUBLISHING_ENCRYPTION_KEY=<64 hex>
ALLOW_REGISTRATION=false
CRON_SECRET=<random string>

Recommended additions:

Terminal window
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=...
SMTP_PASSWORD=...
SMTP_FROM=noreply@orimora.com
DOCS_URL=https://docs.orimora.com

Variables 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.


SettingValue
Build packDockerfile
Base directory/docs
Domainhttps://docs.orimora.com
Port80

The app redirects /docs to DOCS_URL — set that env var on the app service to match.


Migrations run automatically on every container start via run-migrations.mjs. No manual step for upgrades.

To debug inside the container terminal:

Terminal window
node run-migrations.mjs

Each migration runs in its own transaction — a failure on migration 15 does not roll back 1–14.


Correct first-deploy order:

  1. Create the Coolify service (Source: GitHub, Build pack: Docker Compose).
  2. Do not deploy yet. Go to the Environment Variables tab.
  3. 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.
  4. Set ALLOW_REGISTRATION=true temporarily (first user setup).
  5. Click Deploy. The build runs, migrations execute, the app starts.
  6. Complete onboarding at https://orimora.com to create the admin user.
  7. Set ALLOW_REGISTRATION=false again 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.


Migrations run automatically on container start — no manual yarn db:migrate needed. The upgrade workflow is:

  1. In Coolify, click Redeploy (or push a new commit to main if the webhook is configured).
  2. Coolify pulls the latest image, runs the new container alongside the old one (rolling update).
  3. docker-entrypoint.sh calls run-migrations.mjs before the server starts — new schema is applied.
  4. 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.


ProblemCauseFix
Container crashes immediatelyRequired :? var missing (see First deploy)Fill all seven required vars before first deploy
Duplicate empty env varsCoolify auto-creates vars from ${VAR} and you added them manuallyDelete empty duplicates in Coolify UI
EAI_AGAIN / DB timeoutApp not on coolify network or wrong hostnameUse internal URL from Coolify DB resource; hostname is UUID
DB hostname changedRecreated Postgres resourceUpdate DATABASE_URL with new UUID
Boot crash, no clear errorMissing :? required varCheck all seven required vars above
Rate limit affects everyoneXFF_DEPTH wrongKeep default 1 for standard Coolify
Uploads lost on redeployVolume not mountedDo 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 pushCoolify builds on any push to mainSet watch_paths: docs/** in Coolify to scope rebuilds

Coolify → app service → General → HTTP Basic Auth → enable, set credentials, redeploy. Blocks all access at Traefik until you disable it.


ALLOW_REGISTRATION=false (default) blocks open sign-up via magic link or OAuth. The first user (onboarding) and invited users can still join.


RecordTypeValue
orimora.comAServer IP
docs.orimora.comAServer IP

Wildcard *.orimora.com simplifies future subdomains.