Skip to content

Deploy runbook — Coolify

End-to-end runbook for getting Treeper into production on a personal Coolify instance. Covers four apps:

  1. Supabase — self-hosted, deployed via Coolify’s built-in service.
  2. Backend — NestJS, built from apps/backend/Dockerfile.
  3. Workers — Python FastAPI, built from apps/workers/Dockerfile.
  4. Web — Vite SPA (nginx), built from apps/web/Dockerfile (repo-root context). See §3b.

No GitHub Actions required. Coolify ships a GitHub App that auto-deploys on push, and Treeper’s schema migrations are tracked by the Supabase CLI — so you apply them by running one script from your laptop when a migration lands.

flowchart LR
push["git push origin main"]:::user --> gh["GitHub<br/>(Coolify GitHub App)"]
gh --> hookB[["Coolify · backend"]]
gh --> hookW[["Coolify · workers"]]
hookB & hookW -.->|service-role| sb[("Supabase<br/>(also on Coolify)")]
laptop["💻 your laptop<br/>scripts/deploy/apply_migrations.sh"]:::user -.->|"once per schema change"| sb
classDef user fill:#C5E04A,stroke:#13251A;


  • A Coolify instance on a Linux host with Docker (recommended 4 vCPU / 8 GB / 80 GB SSD for all three services).
  • A DNS zone you control. Free subdomains:
    • api.treeper.app → backend
    • supabase.treeper.app → Supabase
    • workers stay internal-only (no DNS needed)
  • Local: git, gh, supabase CLI (brew install supabase/tap/supabase).
  • A single Coolify project named treeper-production to hold all three resources.

Coolify ships Supabase as a one-click service.

  1. + New ResourceService → search Supabase → pick it.
  2. Project: treeper-production.
  3. Domain: supabase.treeper.app. Coolify provisions TLS via Let’s Encrypt automatically.
  4. Generate secrets when prompted. Save the JWT secret, anon key, service role key, and dashboard password to a password manager — the backend and workers env vars need them.

For migrations the local script needs a direct Postgres URL. From Studio → Settings → Database → Connection String → Direct connection:

postgresql://postgres:<PASSWORD>@db.supabase.treeper.app:5432/postgres

Stash this in your password manager too.

From your laptop, push every migration to the new database:

Terminal window
export SUPABASE_DB_URL='postgresql://postgres:[email protected]:5432/postgres'
scripts/deploy/apply_migrations.sh

After this, Supabase’s supabase_migrations table knows about every migration up to today. Future schema changes are incremental — see §5.

Storage bucket: Treeper uses a bucket called trip-attachments. Create it once via Studio → Storage → New bucket. Configure as private — RLS handles access.


  1. + New ResourceApplicationGitHub or Public Repository → paste your repo URL.
  2. Branch: main.
  3. Build Pack: Dockerfile.
  4. Base Directory: apps/backend.
  5. Dockerfile Location: Dockerfile.
  6. Port: 3000.
  7. Domain: api.treeper.app.
  8. Healthcheck path: /v1/health.

Copy apps/backend/.env.example into Coolify’s env panel and fill these:

NODE_ENV=production
PORT=3000
SUPABASE_URL=https://supabase.treeper.app
SUPABASE_ANON_KEY=<from Studio>
SUPABASE_SERVICE_ROLE_KEY=<from Studio — keep secret>
SUPABASE_JWT_SECRET=<from Studio — keep secret>
SUPABASE_JWT_AUDIENCE=authenticated
WORKERS_BASE_URL=http://<workers-internal-hostname>:8000
WORKERS_SHARED_SECRET=<random 32+ chars; MUST equal workers' value>
SENTRY_DSN=<optional>

WORKERS_BASE_URL fills in after step 3.


  1. + New ResourceApplication → same repo, branch main.
  2. Base Directory: apps/workers.
  3. Dockerfile Location: Dockerfile.
  4. Port: 8000.
  5. Domain: leave empty — workers should not be public.
  6. Healthcheck path: /health.

From apps/workers/.env.example:

ENV=production
PORT=8000
WORKERS_SHARED_SECRET=<MUST equal backend's value>
SUPABASE_URL=https://supabase.treeper.app
SUPABASE_SERVICE_ROLE_KEY=<from Studio — keep secret>
# LLM (self-hosted LiteLLM proxy at litellm.itssatya.in)
LLM_PROVIDER=litellm
LITELLM_API_KEY=<virtual key minted in the LiteLLM admin UI>
LITELLM_BASE_URL=https://litellm.itssatya.in/v1
MODEL_ITINERARY_EXTRACT=claude-haiku-4-5-20251001
# Itinerary finder (spec 0004)
ITINERARY_SEARCH_PROVIDER=brave
BRAVE_SEARCH_API_KEY=<from search.brave.com/api>
SENTRY_DSN=<optional>

Once workers is running, copy its internal Coolify hostname (typically http://<resource-name>:8000 on the project network) into the backend’s WORKERS_BASE_URL, then redeploy the backend.


The web frontend is a static Vite SPA served by nginx. Unlike backend/workers it builds from the repo root (it needs packages/contracts), and its config is baked in at build time (VITE_* are inlined by Vite) — so they go in Build Variables, not runtime Environment.

  1. + New ResourceApplication → your treeper repo.
  2. Build Pack: Dockerfile.
  3. Base Directory: / (repo root — required so the build can read packages/contracts).
  4. Dockerfile Location: apps/web/Dockerfile.
  5. Port: 80.
  6. Domain: app.treeper.app (Coolify provisions TLS).

Build Variables (NOT runtime env — Vite inlines at build)

Section titled “Build Variables (NOT runtime env — Vite inlines at build)”
KeyValue
VITE_API_BASEhttps://api.treeper.app/v1 (your backend URL; /v1 optional — the client adds it)
VITE_SUPABASE_URLhttps://supabase.treeper.app
VITE_SUPABASE_ANON_KEYthe Supabase anon key (browser-safe)
VITE_MAP_STYLE(optional) MapLibre style URL; defaults to OpenFreeMap

In Coolify these must be Build-time variables (a.k.a. build args). Runtime env has no effect on a static SPA — changing them requires a rebuild.

Allow the web origin on the backend (CORS)

Section titled “Allow the web origin on the backend (CORS)”

The browser calls the backend cross-origin, so the backend must allow the web domain. On the backend app set:

CORS_ORIGINS=https://app.treeper.app

(comma-separate multiple origins) and redeploy the backend. Without this the web app loads and signs in (that’s browser→Supabase) but every API call fails CORS.


Coolify ships a first-party GitHub App. Once installed, every push to the configured branch auto-deploys the matching apps.

  1. Coolify UI → SourcesGitHubAdd new GitHub App.
  2. Follow the in-app wizard — it’ll redirect to GitHub, you grant the app access to your repo (or your whole personal org), then GitHub sends the install back to Coolify.

For each app (backend, workers):

  1. Open the application → General → change the source from “Public repository” to your installed GitHub App source.

  2. Branch: main.

  3. Enable Automatic Deploy on Push.

  4. (Optional) Watch paths:

    • Backend → apps/backend/**
    • Workers → apps/workers/**

    This makes the backend ignore worker-only changes and vice-versa, saving build minutes.

Every git push origin main that touches a watched path now triggers the matching app’s Coolify deploy. No GitHub Actions minutes consumed.


5. Migrations — automated via the migrator container

Section titled “5. Migrations — automated via the migrator container”

A tiny Coolify application called treeper-migrator lives in the same project as Supabase. Its image bakes infra/supabase/migrations/ and the Supabase CLI; on boot it runs supabase db push --include-all against the internal Postgres hostname (no tunnels, no public exposure), then idles. Coolify auto-redeploys it on every push to main that touches a migration file.

Source: infra/migrator/Dockerfile + infra/migrator/entrypoint.sh.

In the treeper-production project → + New ResourceApplication → same repo / branch main. Fill:

FieldValue
Build PackDockerfile
Base Directory/ (repo root — required so the Dockerfile can COPY infra/supabase/)
Dockerfile Locationinfra/migrator/Dockerfile
Port(leave empty — no HTTP server)
Domains(none)
Healthcheck path(disable — image has its own pgrep check)
Watch pathsinfra/supabase/migrations/**
Auto-deployOn
SUPABASE_DB_URL=postgresql://postgres:<SERVICE_PASSWORD_POSTGRES>@<supabase-db-container-name>:5432/postgres?sslmode=disable

<supabase-db-container-name> is the Postgres container name inside the project network — usually supabase-db-<uuid>. Find it once:

Terminal window
ssh <user>@<coolify-host>
docker ps --filter 'name=supabase-db' --format '{{.Names}}'

Mark SUPABASE_DB_URL as a secret.

The migrator must join the same Docker network as the Supabase service so it can resolve supabase-db-... by name. In Coolify:

  • Migrator app → Network tab → ensure it’s connected to the Supabase service’s project network (Coolify usually auto-joins all resources in the same project; if not, add it manually).

Click Deploy. Watch the Logs tab — you should see:

>> migrator boot
>> target: postgresql://postgres:***@supabase-db-...:5432/postgres?sslmode=disable
>> 12 migration files baked in
Applying migration 0001_trips.sql...
Applying migration 0002_destinations.sql...
...
>> migrations applied successfully
>> idling — re-deploy this container to apply future migrations

The container stays “running” / “healthy” after that.

  1. Land a new infra/supabase/migrations/0013_*.sql in a PR.
  2. Merge to main.
  3. Coolify sees the watched path changed → rebuilds the migrator image (which bakes the new file) → restarts the container → migrations re-apply (the new one runs, prior ones are skipped via the ledger).
  4. Then the backend / workers auto-deploy fires (it watches apps/**). If a backend deploy might land before the migrator finishes, see §5.6.

For schema changes that the new backend code depends on, set the backend’s Pre-Deploy Command in Coolify to wait for the migrator to be healthy:

Terminal window
sh -c 'until docker inspect -f "{{.State.Health.Status}}" treeper-migrator 2>/dev/null | grep -q healthy; do echo waiting on migrator; sleep 2; done'

For additive migrations (new tables, nullable columns) ordering doesn’t matter; the gate above is only worth wiring for destructive or column-rename changes.

If you ever need to apply migrations from your laptop instead (e.g. the migrator is broken), scripts/deploy/apply_migrations.sh still works — combine with an SSH tunnel through Tailscale.


TaskHow
Force a redeployCoolify UI → app → Redeploy.
Rotate workers shared secretUpdate WORKERS_SHARED_SECRET on both apps; redeploy backend then workers.
Live logsCoolify UI → app → Logs.
Roll back codeCoolify UI → app → Deployments → previous successful → redeploy.
Roll back schemaMigrations are append-only. Land a new migration that reverses the change.
DB backupCoolify Supabase service → Backup (or pg_dump against SUPABASE_DB_URL).
Apply migrationsscripts/deploy/apply_migrations.sh (§5).

If you later want migrations gated automatically (e.g. you add collaborators), bring back a CI workflow that:

  1. Runs supabase db push against SUPABASE_DB_URL.
  2. Calls each Coolify app’s Deploy Webhook (Coolify UI → Application → Webhooks) via authenticated curl.

Free alternatives if GitHub Actions minutes are a concern:

  • Self-hosted GitHub Actions runner on the same Coolify VM — free for personal repos.
  • Gitea + Drone or Forgejo Actions on the Coolify host.
  • Coolify’s own scheduled task running supabase db push against a long-lived migration container.

The current setup (Coolify GitHub App + manual apply_migrations.sh) is the cheapest and is what’s wired in by default.

7. SearXNG — self-hosted meta-search (default for spec 0004)

Section titled “7. SearXNG — self-hosted meta-search (default for spec 0004)”

The itinerary finder calls a SearchProvider to discover candidate URLs. The default is SearXNG — a free, self-hosted meta-search that proxies Google / Bing / DuckDuckGo / Wikipedia / Wikivoyage and exposes a uniform JSON API. No API keys, no quotas, no per-request cost — costs only the compute it runs on.

Project treeper-production+ New ResourceService → search SearXNGDeploy.

  • Domain: leave empty for internal-only access. (Or set searx-treeper.itssatya.in if you want a public search UI.)
  • After deploy, the service is reachable inside the Coolify project network at http://searxng:8080 (the exact hostname is shown on the service page under “Internal URL”; copy that for the worker env).

Workers app → Environment → set:

ITINERARY_SEARCH_PROVIDER=searxng
SEARXNG_BASE_URL=http://<searxng-internal-host>:8080

Optional knobs:

SEARXNG_CATEGORIES=general
SEARXNG_ENGINES=google,bing,duckduckgo,wikipedia

Redeploy the workers app. The provider switch is hot — no code change needed in Treeper.

If you want Brave as a backup, keep BRAVE_SEARCH_API_KEY set and flip the provider env to brave. The stub provider always works offline (returns a single fake hit) and is what the worker falls back to when SEARXNG_BASE_URL is unset.