Deploy runbook — Coolify
End-to-end runbook for getting Treeper into production on a personal Coolify instance. Covers four apps:
- Supabase — self-hosted, deployed via Coolify’s built-in service.
- Backend — NestJS, built from
apps/backend/Dockerfile. - Workers — Python FastAPI, built from
apps/workers/Dockerfile. - 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;Contents
Section titled “Contents”- 0. Prerequisites
- 1. Self-host Supabase on Coolify
- 2. Backend application
- 3. Workers application
- 4. Auto-deploy via Coolify GitHub App
- 5. Migrations — manual on schema change
- 6. Day-2 operations
- Appendix · CI-driven deploys (optional)
0. Prerequisites
Section titled “0. Prerequisites”- 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→ backendsupabase.treeper.app→ Supabase- workers stay internal-only (no DNS needed)
- Local:
git,gh,supabaseCLI (brew install supabase/tap/supabase). - A single Coolify project named
treeper-productionto hold all three resources.
1. Self-host Supabase on Coolify
Section titled “1. Self-host Supabase on Coolify”Coolify ships Supabase as a one-click service.
- + New Resource → Service → search Supabase → pick it.
- Project:
treeper-production. - Domain:
supabase.treeper.app. Coolify provisions TLS via Let’s Encrypt automatically. - 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.
Capture the direct DB URL
Section titled “Capture the direct DB URL”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/postgresStash this in your password manager too.
Apply existing migrations once
Section titled “Apply existing migrations once”From your laptop, push every migration to the new database:
scripts/deploy/apply_migrations.shAfter 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.
2. Backend application
Section titled “2. Backend application”- + New Resource → Application → GitHub or Public Repository → paste your repo URL.
- Branch:
main. - Build Pack: Dockerfile.
- Base Directory:
apps/backend. - Dockerfile Location:
Dockerfile. - Port:
3000. - Domain:
api.treeper.app. - Healthcheck path:
/v1/health.
Environment
Section titled “Environment”Copy apps/backend/.env.example
into Coolify’s env panel and fill these:
NODE_ENV=productionPORT=3000
SUPABASE_URL=https://supabase.treeper.appSUPABASE_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>:8000WORKERS_SHARED_SECRET=<random 32+ chars; MUST equal workers' value>
SENTRY_DSN=<optional>WORKERS_BASE_URL fills in after step 3.
3. Workers application
Section titled “3. Workers application”- + New Resource → Application → same repo, branch
main. - Base Directory:
apps/workers. - Dockerfile Location:
Dockerfile. - Port:
8000. - Domain: leave empty — workers should not be public.
- Healthcheck path:
/health.
Environment
Section titled “Environment”From apps/workers/.env.example:
ENV=productionPORT=8000
WORKERS_SHARED_SECRET=<MUST equal backend's value>
SUPABASE_URL=https://supabase.treeper.appSUPABASE_SERVICE_ROLE_KEY=<from Studio — keep secret>
# LLM (self-hosted LiteLLM proxy at litellm.itssatya.in)LLM_PROVIDER=litellmLITELLM_API_KEY=<virtual key minted in the LiteLLM admin UI>LITELLM_BASE_URL=https://litellm.itssatya.in/v1MODEL_ITINERARY_EXTRACT=claude-haiku-4-5-20251001
# Itinerary finder (spec 0004)ITINERARY_SEARCH_PROVIDER=braveBRAVE_SEARCH_API_KEY=<from search.brave.com/api>
SENTRY_DSN=<optional>Wire backend ↔ workers
Section titled “Wire backend ↔ workers”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.
3b. Web application (apps/web)
Section titled “3b. Web application (apps/web)”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.
- + New Resource → Application → your treeper repo.
- Build Pack: Dockerfile.
- Base Directory:
/(repo root — required so the build can readpackages/contracts). - Dockerfile Location:
apps/web/Dockerfile. - Port:
80. - 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)”| Key | Value |
|---|---|
VITE_API_BASE | https://api.treeper.app/v1 (your backend URL; /v1 optional — the client adds it) |
VITE_SUPABASE_URL | https://supabase.treeper.app |
VITE_SUPABASE_ANON_KEY | the 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.
4. Auto-deploy via Coolify GitHub App
Section titled “4. Auto-deploy via Coolify GitHub App”Coolify ships a first-party GitHub App. Once installed, every push to the configured branch auto-deploys the matching apps.
4.1 Install the Coolify GitHub App
Section titled “4.1 Install the Coolify GitHub App”- Coolify UI → Sources → GitHub → Add new GitHub App.
- 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.
4.2 Point both apps at it
Section titled “4.2 Point both apps at it”For each app (backend, workers):
-
Open the application → General → change the source from “Public repository” to your installed GitHub App source.
-
Branch:
main. -
Enable Automatic Deploy on Push.
-
(Optional) Watch paths:
- Backend →
apps/backend/** - Workers →
apps/workers/**
This makes the backend ignore worker-only changes and vice-versa, saving build minutes.
- Backend →
4.3 Done
Section titled “4.3 Done”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.
5.1 Create the migrator app in Coolify
Section titled “5.1 Create the migrator app in Coolify”In the treeper-production project → + New Resource →
Application → same repo / branch main. Fill:
| Field | Value |
|---|---|
| Build Pack | Dockerfile |
| Base Directory | / (repo root — required so the Dockerfile can COPY infra/supabase/) |
| Dockerfile Location | infra/migrator/Dockerfile |
| Port | (leave empty — no HTTP server) |
| Domains | (none) |
| Healthcheck path | (disable — image has its own pgrep check) |
| Watch paths | infra/supabase/migrations/** |
| Auto-deploy | On |
5.2 Environment
Section titled “5.2 Environment”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:
ssh <user>@<coolify-host>docker ps --filter 'name=supabase-db' --format '{{.Names}}'Mark SUPABASE_DB_URL as a secret.
5.3 Network
Section titled “5.3 Network”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).
5.4 First deploy
Section titled “5.4 First deploy”Click Deploy. Watch the Logs tab — you should see:
>> migrator boot>> target: postgresql://postgres:***@supabase-db-...:5432/postgres?sslmode=disable>> 12 migration files baked inApplying migration 0001_trips.sql...Applying migration 0002_destinations.sql......>> migrations applied successfully>> idling — re-deploy this container to apply future migrationsThe container stays “running” / “healthy” after that.
5.5 Adding a migration later
Section titled “5.5 Adding a migration later”- Land a new
infra/supabase/migrations/0013_*.sqlin a PR. - Merge to
main. - 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).
- Then the backend / workers auto-deploy fires (it watches
apps/**). If a backend deploy might land before the migrator finishes, see §5.6.
5.6 Ordering: migrator before backend
Section titled “5.6 Ordering: migrator before backend”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:
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.
5.7 Manual escape hatch
Section titled “5.7 Manual escape hatch”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.
6. Day-2 operations
Section titled “6. Day-2 operations”| Task | How |
|---|---|
| Force a redeploy | Coolify UI → app → Redeploy. |
| Rotate workers shared secret | Update WORKERS_SHARED_SECRET on both apps; redeploy backend then workers. |
| Live logs | Coolify UI → app → Logs. |
| Roll back code | Coolify UI → app → Deployments → previous successful → redeploy. |
| Roll back schema | Migrations are append-only. Land a new migration that reverses the change. |
| DB backup | Coolify Supabase service → Backup (or pg_dump against SUPABASE_DB_URL). |
| Apply migrations | scripts/deploy/apply_migrations.sh (§5). |
Appendix · CI-driven deploys (optional)
Section titled “Appendix · CI-driven deploys (optional)”If you later want migrations gated automatically (e.g. you add collaborators), bring back a CI workflow that:
- Runs
supabase db pushagainstSUPABASE_DB_URL. - 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 pushagainst 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.
7.1 Deploy SearXNG on Coolify
Section titled “7.1 Deploy SearXNG on Coolify”Project treeper-production → + New Resource → Service →
search SearXNG → Deploy.
- Domain: leave empty for internal-only access. (Or set
searx-treeper.itssatya.inif 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).
7.2 Point the worker at it
Section titled “7.2 Point the worker at it”Workers app → Environment → set:
ITINERARY_SEARCH_PROVIDER=searxngSEARXNG_BASE_URL=http://<searxng-internal-host>:8080Optional knobs:
SEARXNG_CATEGORIES=generalSEARXNG_ENGINES=google,bing,duckduckgo,wikipediaRedeploy the workers app. The provider switch is hot — no code change needed in Treeper.
7.3 Fallbacks
Section titled “7.3 Fallbacks”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.
Reference
Section titled “Reference”- ADR 0006 — Docker / Coolify deploy
- Quick-reference setup notes: ./README.md
- Migrations source:
infra/supabase/migrations/ - Manual migration script:
scripts/deploy/apply_migrations.sh