self-host

Run ChiliCMS on your own Postgres with docker-compose or Coolify — free forever, Apache-2.0.

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 NOSUPERUSER and NOBYPASSRLS. 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 -d

Then 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 --apply

The API is now on http://localhost:4000. Check it:

curl http://localhost:4000/health

Open 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.

  1. Create a Postgres 16 resource, or bring your own connection string.
  2. Add a service from ghcr.io/a14a-org/chilicms:latest.
  3. Set DATABASE_URL, APP_DATABASE_URL, and PORT=4000 in the environment.
  4. Set the health check path to /health.
  5. Deploy. Use the generated sslip.io URL 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 --apply

environment

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.

variablerequiredwhat it does
DATABASE_URLyesOwner-role connection string. Runs migrations and pg-boss DDL.
APP_DATABASE_URLyesApp-role connection string. Every request opens a per-transaction RLS session with this role. Must be NOSUPERUSER + NOBYPASSRLS.
PORTnoAPI listen port. Defaults to 4000.
CHILICMS_AUTH_MODEproddb for production magic-link auth. fixture is test-only.
CHILICMS_EMAIL_BACKENDprodsmtp or resend. Decides how magic links go out.
CHILICMS_AUDIT_CHAINrec.Set to 1 to hash-chain the audit log per row.
CHILICMS_STATEMENT_TIMEOUTnostatement_timeout in ms on every RLS session. Defaults to 5000.

One variable is load-bearing for security: APP_DATABASE_URL must point at a non-superuser role with NOBYPASSRLS. 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.

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.com

Use 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 is NOSUPERUSER and NOBYPASSRLS, so the database enforces the tenant boundary, not the application. Every tenant table ships with ENABLE and FORCE ROW LEVEL SECURITY, and middleware sets app.tenant_id per 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.