Operations on Coolify
The production platform is intentionally understandable by one maintainer: one Git-backed Docker Compose application, one managed PostgreSQL resource, private Redis, health checks, and a scheduled prompt-publishing task.
Container roles
| Service | Public? | State | Health meaning |
|---|---|---|---|
verify | No | None; exits | All static checks and builds passed |
migrate | No | Schema changes; exits | Every committed Prisma migration applied |
web | Yes, 3000 | PostgreSQL | HTTP process is ready |
admin | Yes, 3001 | PostgreSQL | Admin HTTP process is ready |
docs | Yes, 3003 | Static files | Learning site can serve generated output |
redis | No | Bounded AOF volume | Authenticated Redis responds |
worker | No | Redis transport | Worker accepts jobs and Redis is ready |
deploy-watcher | No | None | Polls main and triggers the next release |
Why Compose health dependencies matter
Starting a process is not the same as making a release ready. Compose uses explicit conditions:
web:
depends_on:
migrate:
condition: service_completed_successfully
admin:
depends_on:
web:
condition: service_healthy
docs:
depends_on:
verify:
condition: service_completed_successfullyIf verification or migration fails, new database-dependent application containers do not replace the healthy release with an incompatible version. Static docs still avoid a false database dependency: they wait for verification, not migration.
Static docs deployment
VitePress generates HTML, CSS, JavaScript, a local search index, and a sitemap during the release build. The runtime image contains only Node and those generated files. It has no production environment secrets, database client, or application source.
FROM node:24-bookworm-slim AS docs
WORKDIR /app
COPY --from=release-build /app/apps/docs/server.mjs ./server.mjs
COPY --from=release-build /app/apps/docs/.vitepress/dist ./site
USER node
CMD ["node", "server.mjs"]The small server handles clean URLs, cache headers, security headers, and /health. Content-hashed assets cache for a year; HTML always revalidates so a new lesson appears immediately after deployment.
Scheduled prompt publishing
Coolify runs one daily task at 0 12 * * * UTC inside the admin container:
node /app/scripts/run-publish-due-prompts.mjsThe script calls the protected admin endpoint using CRON_SECRET and exits non-zero on failure. Scheduling changes lifecycle state; it does not fan out an assignment to every couple. A couple's stable daily assignment is created idempotently on read.
Push-to-deploy without public control-plane access
The Coolify control plane is tailnet-only. A private deploy-watcher polls the repository's main ref using a read-only deploy key. When the commit changes, it signs a minimal GitHub push payload and submits it to Coolify's private manual-webhook endpoint.
This avoids exposing the dashboard or relying on a paid external runner. The tradeoff is a small polling process whose credential and webhook health must be checked when deployments stop moving.
Production diagnostics
Start with the cheapest discriminators:
curl --fail https://www.everduet.com/api/health
curl --fail https://admin.everduet.com/api/health
curl --fail https://docs.everduet.com/healthThen inspect the latest deployment, container health, and structured logs in Coolify. Use x-request-id to correlate a request without copying private content.
Migration posture
Migrations are forward-only in production:
- commit a Prisma migration with the code;
- let the release gate apply it before new services start;
- make additive changes when a staged rollout needs old and new code compatibility;
- correct a bad deployed schema with a new migration.
Editing an already applied migration destroys the audit trail and makes environments disagree about what “applied” means.
What this platform deliberately does not add
- Kubernetes or a service mesh;
- independent service release trains;
- a second database authority;
- paid monitoring as a release requirement;
- automatic host pruning that can destroy useful state;
- queue-driven product work without a measured reason.
The platform can grow when a real constraint appears. Until then, simplicity is an operational feature.
Next: tradeoff explorer.