concile
Deploy & Operate

Postgres

Point concile at a Postgres database instead of SQLite, with no code changes.

SQLite is our awesome, zero-config default for concile serve. It runs completely from a single file, so you don't have to worry about installing extra software. But hey, if you prefer using Postgres, you can easily make the switch! You'll still get the exact same reactive engine and app code without rewriting a single line, but your data gets tucked away safely in a robust database that your infrastructure is likely already backing up, replicating, and monitoring for you.

In this guide, we'll walk you through how to turn it on, what happens behind the scenes when it boots up, the inner workings of the engine, our single-writer approach, group commits, and share a few thoughts on performance.

Turning it on

To get started, just point your serve or dev command at a Postgres database using the --database-url flag. You can also use the CONCILE_DATABASE_URL environment variable. If you happen to set both, the flag takes priority. If you leave both blank, the system will happily stick with SQLite.

concile serve --dir concile --database-url postgres://user:pass@host:5432/db

Any connection string that starts with postgres:// or postgresql:// will automatically select the Postgres backend. Anything else will fall back to SQLite. It is wonderful that this choice happens at boot time, meaning it is not hardcoded into your application. This works perfectly for concile dev as well as any compiled concile build binary.

Using Docker Compose with a postgres:16 service

If you are using Docker, you can simply add a postgres service to your docker-compose.yml file. Then, just swap out the SQLite volume for your CONCILE_DATABASE_URL:

docker-compose.yml
services:
  concile:
    build:
      context: .
      target: runner
    ports:
      - "3000:3000"
    volumes:
      - ./concile:/app/concile:ro
    environment:
      CONCILE_ADMIN_KEY: ${CONCILE_ADMIN_KEY}
      CONCILE_DATABASE_URL: postgres://concile:concile@postgres:5432/concile
    command: serve --dir /app/concile
    depends_on:
      - postgres

  postgres:
    image: postgres:16
    restart: unless-stopped
    environment:
      POSTGRES_USER: concile
      POSTGRES_PASSWORD: concile
      POSTGRES_DB: concile
    volumes:
      - postgres-data:/var/lib/postgresql/data

volumes:
  postgres-data:

Now, when you run docker compose up, your app will save everything to the postgres-data named volume instead of the old concile-data one. Everything else from our Self-hosting guide, like the admin key, the dashboard, and restart persistence, works exactly the same!

What happens at boot

Every time you boot the engine, it automatically runs setupSchema(). This handy process safely executes a series of "create if not exists" statements for our internal tables and sequences. Don't worry, it only touches a fixed set of internal tables (which we explain below) and never messes with your own app's tables.

This process is perfectly safe to run on every boot. If the database already has these tables set up, the engine will quickly glide past those steps without making any changes.

Once the initial setup is complete, the engine claims a single-writer advisory lock. The very first time it connects to a database, it also sets up the commit-timestamp sequence so it can pick right up where it left off (or start fresh at 1). The best part? There is no manual setup required from you. Just pointing serve at an empty Postgres database is all it takes!

The single-writer invariant

To keep your data safe, only one concile engine can connect to a specific Postgres database at any given time. When it boots up, the engine secures a pg_advisory_lock, which is tied directly to its pinned connection. If another serve or dev process tries to connect to the same database, it will immediately fail and let you know, preventing any accidental data corruption:

$ concile serve --dir concile --database-url postgres://...   # already running elsewhere
Error: another Concile engine is already connected to this database (advisory lock held)

This setup gives you fantastic single-node durability. It allows you to use Postgres as a powerful, externally-managed database for one writer. However, it is not designed for clustering or running multiple engines simultaneously for high availability. If you are interested in multi-node scaling, be sure to check out the Scaling guide for details on concile serve --fleet.

Known limitations

Single pinned connection, no automatic reconnect. The engine relies on exactly one Postgres connection for its entire lifespan. This connection handles the essential single-writer lock and transaction pinning. If the connection drops due to a network glitch or a database restart, the engine won't reconnect on its own. You will just need to quickly restart the concile process to get things back on track.

An unclean process kill can briefly hold the lock. If the process shuts down gracefully (SIGTERM), it lets go of the lock instantly. But if it crashes or is forcefully killed (SIGKILL), the lock might linger for a few seconds until Postgres realizes the session is gone. If you restart immediately, you might briefly see an error saying "another engine is already connected."

Keep in mind that these aren't limitations on clustering or high availability. They are simply natural outcomes of our single-node, single-writer design.

Going deeper

  • Self-hosting with Docker: The baseline concile serve and docker compose up setup that this backend drops right into. Your admin key, dashboard, and restart persistence all work exactly the same.
  • Scaling: Learn about concile serve --fleet, which lets multiple nodes share this Postgres backend for scaled out writes and live failover.
  • Configuration reference: A complete list of flags and environment variables, including CONCILE_DATABASE_URL and CONCILE_GROUP_COMMIT.

On this page