self-host
Self-hosting ChiliCMS is free forever. The core is Apache-2.0 — no feature paywall, no seat caps, no CLA, no phone-home. You bring a Postgres 16+ and one container; ChiliCMS does the rest. This page takes you from nothing to a running instance, then covers the env, the database roles, the license, and the offline license note for the enterprise modules.
what you need
- Postgres 16 or newer. SQLite is dev-only; production is Postgres.
- One container. The published image is
ghcr.io/a14a-org/chilicms:latest. - Two database roles (covered below) — an owner role for migrations and an
app role that runs every request with
NOSUPERUSERandNOBYPASSRLS. This is what makes row-level security the law instead of a suggestion.
The API binds 0.0.0.0 and listens on PORT (default 4000). It serves REST
at /api/v1/{posts,pages,assets}, GraphQL at /graphql, auth at
/auth/signin, and a health probe at /health.
run it with docker-compose
This is the whole stack: Postgres plus the API, on one network.
name: chilicms
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: chilicms_owner
POSTGRES_PASSWORD: change_me
POSTGRES_DB: chilicms
volumes:
- chilicms_pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U chilicms_owner -d chilicms"]
interval: 2s
timeout: 5s
retries: 20
restart: unless-stopped
api:
image: ghcr.io/a14a-org/chilicms:latest
depends_on:
db:
condition: service_healthy
ports:
- "4000:4000"
environment:
PORT: 4000
DATABASE_URL: postgres://chilicms_owner:change_me@db:5432/chilicms
APP_DATABASE_URL: postgres://chilicms_app:change_me_too@db:5432/chilicms
restart: unless-stopped
volumes:
chilicms_pgdata:Bring it up:
docker compose up -dThen apply the schema and its RLS policies against the database. pepper is the
ChiliCMS CLI; --apply runs the generated migration and the forced row-level
security policies.
docker compose exec api pepper push --applyThe API is now on http://localhost:4000. Check it:
curl http://localhost:4000/healthOpen the Studio and create the first admin with the magic link. In dev the link is printed to the container logs; in production it is emailed (see email below).
run it on Coolify
ChiliCMS deploys cleanly behind Coolify’s proxy because the API already binds
0.0.0.0. Point a new resource at the image (or your fork), set the two
database URLs and PORT, and let Coolify build and run it.
- Create a Postgres 16 resource, or bring your own connection string.
- Add a service from
ghcr.io/a14a-org/chilicms:latest. - Set
DATABASE_URL,APP_DATABASE_URL, andPORT=4000in the environment. - Set the health check path to
/health. - Deploy. Use the generated
sslip.ioURL first; flip to your own domain once it is green.
Run the migration once after the first deploy, from Coolify’s terminal for the service:
pepper push --applyenvironment
ChiliCMS reads its configuration from environment variables. Copy
.env.example and fill in your values; .env.local is gitignored. These are
the variables a self-host actually needs — the full list with phase notes lives
in .env.example at the repo root.
| variable | required | what it does |
|---|---|---|
DATABASE_URL | yes | Owner-role connection string. Runs migrations and pg-boss DDL. |
APP_DATABASE_URL | yes | App-role connection string. Every request opens a per-transaction RLS session with this role. Must be NOSUPERUSER + NOBYPASSRLS. |
PORT | no | API listen port. Defaults to 4000. |
CHILICMS_AUTH_MODE | prod | db for production magic-link auth. fixture is test-only. |
CHILICMS_EMAIL_BACKEND | prod | smtp or resend. Decides how magic links go out. |
CHILICMS_AUDIT_CHAIN | rec. | Set to 1 to hash-chain the audit log per row. |
CHILICMS_STATEMENT_TIMEOUT | no | statement_timeout in ms on every RLS session. Defaults to 5000. |
One variable is load-bearing for security:
APP_DATABASE_URLmust point at a non-superuser role withNOBYPASSRLS. If it does not, the app can read across tenants and row-level security stops protecting anything. Never run app code as a Postgres superuser.
email for magic-link
The first admin signs in with a magic link, so production needs a way to send mail. Two backends:
# Plain SMTP — any relay.
CHILICMS_EMAIL_BACKEND=smtp
CHILICMS_SMTP_HOST=smtp.example.com
CHILICMS_SMTP_PORT=587
CHILICMS_AUTH_FROM=no-reply@yourdomain.com
# Or Resend's HTTP API. Verify the sender domain in Resend first.
CHILICMS_EMAIL_BACKEND=resend
CHILICMS_RESEND_API_KEY=re_xxx
CHILICMS_AUTH_FROM=no-reply@yourdomain.comUse a sender domain you control SPF/DKIM for, or DMARC will quarantine the links.
the two Postgres roles
ChiliCMS uses two roles on purpose. The split is the entire point of being Postgres-native:
- Owner role (
DATABASE_URL) owns the tables and runs migrations. It is the only role that touches DDL. Migrations create the app role and grant it exactly the table privileges it needs. - App role (
APP_DATABASE_URL) serves traffic. It isNOSUPERUSERandNOBYPASSRLS, so the database enforces the tenant boundary, not the application. Every tenant table ships withENABLEandFORCE ROW LEVEL SECURITY, and middleware setsapp.tenant_idper transaction from a verified JWT.
You do not create these roles by hand. pepper push --apply runs the
migrations that create chilicms_app, grant its privileges, and turn RLS on.
Point the owner URL at a fresh database and run it once.
the license
The core is Apache-2.0. Everything in apps/ and packages/ is Apache-2.0,
patent grant included. Self-hosting the core costs nothing and always will —
that is a brand commitment, not a tier.
The enterprise/ modules (SSO/SAML/SCIM, multi-tenant orchestration, extended
audit retention) ship under BSL 1.1 with a four-year auto-convert: each
release becomes Apache-2.0 four years after it ships. You do not need any
enterprise module to run a full, production ChiliCMS. They are additive, not a
gate on what was open at launch.
Contributions are DCO only — sign off your commits with git commit -s.
There is no CLA, so your contributions are never relicensed out from under you.
the offline license note
The enterprise modules verify their license offline. The check is a local signature verification against a license file you hold — there is no phone-home, no usage beacon, nothing reaching back to us. An enterprise ChiliCMS runs fully air-gapped, and the core never asks for a key at all.
If you only self-host the Apache-2.0 core — which is everything most teams need — there is no license file and nothing to install. Your deployment stays yours.
Next: read the concepts behind the delivery model, or wire up the typed sdk.