Database

Pengin-Pi-3 uses PostgreSQL for relational data and FerretDB for documents. Both live in one PostgreSQL instance — FerretDB is a gateway speaking the MongoDB wire protocol against a Postgres database with the DocumentDB extension installed.

┌────────────────────────────────────────┐
│  PostgreSQL                            │
│    ${FERRETDB_DB}  ← documentdb ext    │
│    ${DB_NAME}      ← Django's tables   │
└────────┬──────────────────┬────────────┘
         │                  │
  psycopg2 (SQL)     FerretDB gateway
         │                  │ :27017
      Django ──── pymongo ──┘

One instance, one volume, one backup. That is the main reason for FerretDB over MongoDB here.

Environment variables are covered in Configuration; this page is about getting the services themselves running.


You may not need any of this

settings.py falls back to a local SQLite file when Postgres details are incomplete. Clone, install requirements, migrate, runserver — you have a working instance with no database server at all.

What you lose is the document store. Dynamic content types (Document Store) need FerretDB and will raise ConnectionFailure without it. Everything else works.

If you're contributing to the core and not touching dynamic slugs, skip to Migrations.


The compose stack handles all of this. Two services:

  • postgresghcr.io/ferretdb/postgres-documentdb:17-..., which is stock Postgres 17 with the DocumentDB extension already built in and configured
  • ferretdbghcr.io/ferretdb/ferretdb:2.7.0, the gateway

On first boot the image's own init scripts install the extension, and docker-entrypoint-initdb/01-create-app-db.sh creates Django's database alongside it. Nothing else to do.

The image is not optional. A stock postgres:17-alpine starts cleanly and then fails every document operation with schema "documentdb_api" does not exist. See Deployment.

The rest of this page is for running the services directly on a machine.


PostgreSQL

Fedora

sudo dnf install postgresql-server postgresql postgresql-contrib
sudo dnf install libpq-devel gcc python3-devel   # for psycopg2

sudo postgresql-setup --initdb
sudo systemctl enable --now postgresql

Fedora's initdb defaults to ident authentication for local connections, which Django can't use. Edit /var/lib/pgsql/data/pg_hba.conf and change the host lines for 127.0.0.1/32 and ::1/128 from ident to scram-sha-256, then:

sudo systemctl restart postgresql

Debian and Ubuntu

sudo apt install postgresql postgresql-contrib
sudo apt install libpq-dev build-essential python3-dev

sudo systemctl enable --now postgresql

Debian and Ubuntu initialize the cluster on install and default to scram-sha-256 for host connections, so no pg_hba.conf edit is usually needed.

Windows

Use WSL2 with Fedora, Debian, or Ubuntu and follow the instructions above inside it. The deployment scripts assume a POSIX environment, and native Windows Postgres plus FerretDB is not a combination anyone here has tested.

macOS

Untested by the maintainers. Homebrew provides postgresql@17; libpq usually needs adding to your path before pip can build psycopg2. For FerretDB, use Docker Desktop.


User and database

Create a role and a database for the application:

sudo -u postgres psql

CREATE USER penginpi WITH PASSWORD 'choose-something-real';
CREATE DATABASE penginpi OWNER penginpi;
GRANT ALL PRIVILEGES ON DATABASE penginpi TO penginpi;
\q

On Postgres 15 and later the role also needs schema rights, which are no longer implied by database ownership:

sudo -u postgres psql -d penginpi -c \
  "GRANT ALL ON SCHEMA public TO penginpi;"

Skipping that produces permission denied for schema public on the first migration.

Do not create tables by hand. Django's migrations own the schema — users, slugs, events, history tables, everything. Your job is the role and the empty database.

Verify the login works:

psql -h 127.0.0.1 -U penginpi -d penginpi -c '\conninfo'

FerretDB

Two pieces, and they must be installed in this order: the DocumentDB extension into PostgreSQL, then the FerretDB gateway.

This is the fiddly part of a bare-metal setup. If it fights you, run just these two as containers alongside your local Postgres — or run the whole stack in Docker and develop against it.

1. DocumentDB extension

Packages are published on the FerretDB/documentdb releases page.deb for Debian and Ubuntu, .rpm for RHEL-family systems. Download the package matching your PostgreSQL major version and your FerretDB version; the two are released in step and mismatches are not supported.

Take the production package, not the -dev or -dbgsym variants — those carry debugging features that significantly slow things down.

Debian / Ubuntu

sudo apt install postgresql-17-cron postgresql-17-rum \
                 libmongocrypt0 libbson-1.0-0
sudo dpkg -i /path/to/documentdb.deb

Fedora. Fedora is not an official RPM target — the published .rpm packages target RHEL and CentOS, and FerretDB's own docs still mark RPM support experimental. A RHEL package for your Postgres major version will often install on Fedora, but you may hit dependency mismatches on pg_cron, rum, or the Mongo C driver. If it doesn't install cleanly, run FerretDB's Postgres image in a container rather than fighting it:

docker run -d --name pengin-pg \
  -e POSTGRES_USER=penginpi \
  -e POSTGRES_PASSWORD=choose-something-real \
  -e POSTGRES_DB=postgres \
  -p 5432:5432 \
  -v pengin_pg_data:/var/lib/postgresql/data \
  ghcr.io/ferretdb/postgres-documentdb:17-0.107.0-ferretdb-2.7.0

That gives you a correctly configured Postgres with the extension already in place, and you create the application database inside it exactly as above.

2. Configure and load the extension

Add to postgresql.conf:

shared_preload_libraries = 'pg_cron,pg_documentdb_core,pg_documentdb'
cron.database_name = 'postgres'
documentdb.enableCompact = true

shared_preload_libraries only takes effect on a full restart, not a reload:

sudo systemctl restart postgresql

Then create the extension in the database named by cron.database_name:

sudo -u postgres psql -d postgres -c \
  'CREATE EXTENSION IF NOT EXISTS documentdb CASCADE;'

CASCADE pulls in documentdb_core, pg_cron, and the other dependencies.

This is why FERRETDB_DB and DB_NAME are separate settings. The extension installs into one database via pg_cron; Django's tables live in another. Both are in the same instance. Keep FERRETDB_DB=postgres and cron.database_name = 'postgres' matching.

Verify:

sudo -u postgres psql -d postgres -c '\dx'

You want documentdb, documentdb_core, and pg_cron listed.

3. FerretDB gateway

Download from the FerretDB releases page.

sudo dpkg -i ferretdb.deb        # Debian / Ubuntu
sudo rpm -i ferretdb.rpm         # RPM-based
ferretdb --version

The packages ship a systemd unit. Point it at Postgres:

FERRETDB_POSTGRESQL_URL=postgres://penginpi:PASSWORD@127.0.0.1:5432/postgres

Note the database in that URL is FERRETDB_DB — the one holding the extension — not the application database.

sudo systemctl enable --now ferretdb

Or skip the package and run it as a container against your local Postgres:

docker run -d --name pengin-ferretdb -p 27017:27017 \
  -e FERRETDB_POSTGRESQL_URL="postgres://penginpi:PASSWORD@host.docker.internal:5432/postgres" \
  ghcr.io/ferretdb/ferretdb:2.7.0

Wiring Django to it

In .env:

DB_ENGINE=django.db.backends.postgresql
DB_NAME=penginpi
DB_USER=penginpi
DB_PASSWORD=choose-something-real
DB_HOST=127.0.0.1
DB_PORT=5432

FERRETDB_DB=postgres
MONGODB_URI=mongodb://penginpi:choose-something-real@127.0.0.1:27017/
MONGODB_DB_NAME=penginpi

All four of DB_NAME, DB_USER, DB_PASSWORD, and DB_HOST are required together. Any one missing and settings falls back to SQLite with a printed warning — the app starts, so the only symptom is that your data isn't where you think it is. Check the startup log.

Full reference in Configuration.


Migrations

The whole project is a single Django app (main) with one migrations directory, main/migrations/. There is no cross-app migration coordination, and every model — User, Slug, Event, TeamRole, all the history models — shares the main app label.

First run

python manage.py migrate
python manage.py createsuperuser

Under Docker the web container runs collectstatic and migrate automatically on every start, so a fresh stack is already migrated. You still need to create the superuser yourself:

docker compose exec web python manage.py createsuperuser

After changing models

python manage.py makemigrations
python manage.py migrate

Being one app, a bare makemigrations picks up everything. makemigrations main is equivalent and more explicit.

Review the generated file before committing it. Adding a non-nullable field to a populated table will prompt for a default — answer it deliberately rather than accepting whatever's offered, since that value gets written into every existing row.

Useful checks:

python manage.py makemigrations --check --dry-run   # anything unmigrated?
python manage.py showmigrations                     # what's applied
python manage.py sqlmigrate main 0012               # the SQL it will run

Under Docker

docker compose exec web python manage.py makemigrations
docker compose exec web python manage.py migrate

The project directory is not bind-mounted into the web container — only media, staticfiles, and the blocklist are. A migration generated inside the container lives only in that container and vanishes on rebuild. Generate migrations locally, commit them, then rebuild.

Migrations run automatically on container start

Fine for a single web container, which is the shipped configuration. If you scale to several, they will race — split migrations into a separate step before the rollout at that point.

Seed data

python manage.py seed_departments

Creates example departments and titles for the RBAC framework. Edit the lists for your own project. See RBAC.

There's one convention worth preserving: every department needs an "Employee" title, because the role-assignment form falls back to it. The seed command establishes that; departments you create by hand should follow.


Verifying

Postgres

python manage.py shell -c \
  "from django.db import connection; print(connection.vendor); \
   print(connection.settings_dict['NAME'])"

Should print postgresql and your database name. sqlite means the fallback fired.

FerretDB

python manage.py shell -c \
  "from util.ferretdb import get_mongo_db; \
   print(get_mongo_db().list_collection_names())"

An empty list is correct on a fresh install — collections are created lazily on first write. A ConnectionFailure means the gateway isn't reachable.

Direct inspection

mongosh "mongodb://penginpi:PASSWORD@127.0.0.1:27017/penginpi"
> show collections
> db.slug_dynamic_data.find().limit(5)

Common problems

| Symptom | Cause | |---|---| | schema "documentdb_api" does not exist | Extension not installed, or installed in a database other than cron.database_name | | permission denied for schema public | Postgres 15+ needs the explicit schema grant | | Running on SQLite unexpectedly | One of the four DB_* values is blank — check the startup log | | ConnectionFailure: Could not connect to FerretDB | Gateway down, or MONGODB_URI wrong. Fails after a 5s timeout. | | psycopg2 won't build | libpq-devel / libpq-dev missing before pip install | | FATAL: Ident authentication failed | Fedora's default pg_hba.conf — switch to scram-sha-256 | | pg_cron errors on start | shared_preload_libraries needs a full restart, not a reload | | Migration exists in container, not in git | Generate migrations locally; the source isn't bind-mounted |


Backup

One dump covers both stores — they're the same instance:

pg_dump -h 127.0.0.1 -U penginpi penginpi > app.sql
pg_dump -h 127.0.0.1 -U penginpi postgres > documents.sql

Or everything at once:

pg_dumpall -h 127.0.0.1 -U penginpi > full.sql

Under Docker:

docker compose exec postgres pg_dumpall -U "$DB_USER" > full.sql

Take a backup before any git pull that includes migrations. Django migrations are not reliably reversible, and the container applies them on start without asking.

Two things that grow and want watching: history snapshots write a full copy of an object's fields on every tracked edit (History and Auditing), and with local file storage, uploads accumulate in media/ with nothing pruning them (Configuration).

See Backup and Restore.


Next