feat(deployment): add Coolify Compose templates for Postgres and SQLite - #475
Open
flyingwebie wants to merge 1 commit into
Open
feat(deployment): add Coolify Compose templates for Postgres and SQLite#475flyingwebie wants to merge 1 commit into
flyingwebie wants to merge 1 commit into
Conversation
Coolify could not run any of the existing Compose files. It treats one
file as the single source of truth, so the `-f compose.prod.yml -f
compose.sqlite.yml -f compose.tls.yml` layering has nowhere to go; it
runs its own Traefik, so the Caddy container and its `./Caddyfile` bind
mount are redundant; and `ports:` would publish Postgres on the host,
bypassing the proxy the platform exists to manage. Two standalone files
instead, one per database engine.
PUBLIC_ORIGIN is the reason these files are worth shipping rather than
leaving to each operator. `resolvePublicOrigins` auto-detects Render and
Railway and has no Coolify branch, and `server/auth/security.ts`
deliberately never trusts X-Forwarded-Proto. Coolify's Traefik terminates
TLS and forwards plain HTTP, so an unset PUBLIC_ORIGIN leaves the server
believing it is http:// and four things degrade — only one of them
loudly: the session cookie silently drops its Secure flag, the CSRF check
compares the wrong expected origin, real-time co-editing never connects
because the WebSocket upgrade runs that same origin guard, and MCP
connector URLs resolve local-only. Wiring `SERVICE_URL_INSTATIC_3001`
alongside `PUBLIC_ORIGIN=${SERVICE_URL_INSTATIC}` fixes all four and
follows a custom domain set later in the UI.
INSTATIC_SECRET_KEY is the other one that has to be right the first time.
The image runs NODE_ENV=production, where masterKey.ts throws at boot
unless the value decodes to exactly 32 bytes. Coolify's REALBASE64_32
emits base64 of 32 random bytes; the similarly-named BASE64_32 emits a
bare 32-character string that is not base64 at all and would fail
validation. Its own Plausible template uses REALBASE64_32 for an
identically-shaped key, so this is precedent rather than inference.
The SQLite template mounts /app/uploads and /app/data rather than the
single /app/storage root the Railway and Render templates use. That
layout exists because those platforms allow one disk, and it needs
RAILWAY_RUN_UID=0 to work at all: a volume mounted where the image has
no directory is created root-owned, and the non-root `bun` user cannot
write to it. The Dockerfile already creates and chowns both of these
paths before dropping privileges.
The gate in dockerConfig.test.ts covers the invariants that fail
silently rather than loudly — a stray `networks:` block (Coolify's
documented cause of intermittent HTTPS outages), a `ports:` mapping, a
missing PUBLIC_ORIGIN, the wrong base64 variable, or a drift back to
/app/storage. Comments carry the reasoning so the next reader knows why
the rule exists.
Verified against the published image rather than by inspection: both
stacks reach healthy, the Postgres stack applies 24 migrations into 38
tables, volumes come up bun-owned and writable, /health answers 200, and
no host port is published. Documented that the image is linux/amd64 only,
which is not incidental — pulling it on arm64 fails outright.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds Coolify as a documented deployment target, alongside the existing Railway, Render, VPS Compose, and generic Docker paths.
What changed
docker-compose.coolify.yml— bundled Postgres 16 stack, app gated on a Postgres readiness healthcheck.docker-compose.coolify.sqlite.yml— single-container SQLite stack.docs/deployment/coolify.md— setup walkthrough, database trade-offs, domains/PUBLIC_ORIGIN, secret-key handling, persistence, updating, ARM64 notes.docs/deployment/README.md— Coolify row in the TL;DR table, docs inventory, and Related file list.scripts/build-release-bundle.ts— ships both Compose files andcoolify.mdin the release bundle, with a Coolify section in the generated install notes.src/__tests__/server/dockerConfig.test.ts— acoolify docker configsuite gating the invariants below.Why Coolify needs its own Compose files
Coolify reads a single Compose file as the source of truth — there is no
-f a.yml -f b.ymloverlay mechanism — so these are standalone rather than a layer oncompose.prod.yml. Both files deliberately omitnetworks:,ports:,container_name:, andrestart:, because Coolify owns the per-stack bridge network, the Traefik route, and the container lifecycle. Declaring any of them fails silently rather than loudly (a second network makes Traefik route non-deterministically; a published port bypasses the proxy and would expose Postgres on the VPS), so the test suite gates them instead of leaving it to review.Two settings are load-bearing and easy to get wrong:
PUBLIC_ORIGIN=${SERVICE_URL_INSTATIC}— Coolify's Traefik terminates TLS and forwards plain HTTP, andserver/auth/security.tsdeliberately never trustsX-Forwarded-Proto/Host.resolvePublicOrigins()inserver/config.tsauto-detects Render and Railway but has no Coolify branch, so without this the server believes it ishttp://and four things degrade — only one of them loudly: the session cookie loses itsSecureflag, the CSRF origin check compares the wrong origin, the collab WebSocket upgrade is rejected by that same guard, and MCP connector URLs resolve local-only.INSTATIC_SECRET_KEY=${SERVICE_REALBASE64_32_INSTATIC}— the image runsNODE_ENV=production, whereserver/secrets/masterKey.tsfails boot unless the key decodes to exactly 32 bytes. Coolify'sREALBASE64_32emits base64 of 32 random bytes; the similarly namedBASE64_32emits a bare 32-character string that is not base64 and would fail validation.The SQLite stack mounts
/app/uploadsand/app/dataas two named volumes rather than one shared root, because theDockerfilecreates exactly those two paths and chowns them tobunbeforeUSER bun. A volume mounted at a path the image does not contain — notably the single/app/storageroot the Railway and Render templates use — is created root-owned and every write fails withEACCES. Railway works around that withRAILWAY_RUN_UID=0; there is no equivalent escape hatch here.Impact
Operators get a Coolify install that is deploy-and-assign-a-domain, with Coolify generating the Postgres password and master key once and keeping them stable across redeploys. No runtime, server, or schema code changes — this is deployment configuration, docs, and tests only.
Verification
Branch is rebased onto current
mainand contains a single commit.🤖 Generated with Claude Code