Why PostgreSQL with Django?

Django ships with SQLite by default, and SQLite is genuinely great for prototyping. But the moment you start thinking about production — concurrent writes, full-text search, JSON fields, robust migrations, real backups — you’ll want a proper database. PostgreSQL is the most feature-rich open-source database out there, and the Django ORM treats it as a first-class citizen. Things like ArrayField, JSONField with indexable queries, HStoreField, and full-text search live inside django.contrib.postgres precisely because the Django team optimized for Postgres.

In this post we’ll walk through the steps to set up a PostgreSQL database, create a dedicated user, configure sane defaults, and wire it all up to your Django project.

Prerequisites

Before you start, make sure you have:

  • PostgreSQL installed locally (brew install postgresql on macOS, sudo apt install postgresql on Debian/Ubuntu).
  • A working Python 3.9+ environment.
  • A Django project (or one you can spin up with django-admin startproject).
  • The psycopg2-binary driver installed: pip install psycopg2-binary. For production builds you’d typically prefer psycopg2 (compiled from source) so you can match the system’s libpq version, but psycopg2-binary is fine for development.

Once Postgres is running, drop into the interactive shell as the superuser:

sudo -u postgres psql

On macOS with Homebrew, you can usually just run psql postgres since Homebrew sets up your user as a superuser by default.

Step 1: Create a PostgreSQL Database

CREATE DATABASE yourdbname;

This command creates a new PostgreSQL database named yourdbname. Pick something meaningful — myproject_dev, blog_prod, etc. — so you don’t end up staring at a list of test1, test2, test_final six months from now.

Step 2: Create a PostgreSQL User

CREATE USER yourdbuser WITH PASSWORD 'yourdbpassword';

This creates a new PostgreSQL user (technically a “role”) named yourdbuser. Don’t use your superuser for the application — give Django its own dedicated role with the least privileges it needs. If the app is ever compromised, the blast radius is contained.

Step 3: Configure User Encoding

ALTER ROLE yourdbuser SET client_encoding TO 'utf8';

This sets the character encoding for yourdbuser to UTF-8. Django expects UTF-8 everywhere, and emoji-laden user content will silently break if you skip this.

Step 4: Configure Transaction Isolation

ALTER ROLE yourdbuser SET default_transaction_isolation TO 'read committed';

read committed is the level Django expects. It means a query inside a transaction sees only rows committed before the query started — preventing dirty reads but still allowing non-repeatable reads. This matches PostgreSQL’s default, but setting it explicitly avoids surprises if the cluster default ever changes.

Step 5: Configure Timezone

ALTER ROLE yourdbuser SET timezone TO 'UTC';

Always store timestamps in UTC. Convert to the user’s local time only at the presentation layer. If you ever debug a timezone-related bug at 2am you will thank past-you for this line.

Step 6: Set Database Owner

ALTER DATABASE yourdbname OWNER TO yourdbuser;

This makes yourdbuser the owner of yourdbname, so Django can create tables, run migrations, and manage schemas without GRANT gymnastics.

Step 7: Connect to Django

Install the driver

pip install psycopg2-binary python-decouple

python-decouple (or django-environ) is a small library for reading config from environment variables — far better than hard-coding secrets into settings.py.

Update settings.py

from decouple import config

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": config("DB_NAME"),
        "USER": config("DB_USER"),
        "PASSWORD": config("DB_PASSWORD"),
        "HOST": config("DB_HOST", default="localhost"),
        "PORT": config("DB_PORT", default="5432"),
    }
}

Create a .env file

DB_NAME=yourdbname
DB_USER=yourdbuser
DB_PASSWORD=yourdbpassword
DB_HOST=localhost
DB_PORT=5432

Run migrations

python manage.py migrate

If you see psycopg2.OperationalError: FATAL: password authentication failed, double-check your password and that the user actually exists (\du in psql). If you see could not connect to server, the Postgres service probably isn’t running.

All SQL Commands in one block

Copy-paste friendly:

CREATE DATABASE yourdbname;
CREATE USER yourdbuser WITH PASSWORD 'yourdbpassword';
ALTER ROLE yourdbuser SET client_encoding TO 'utf8';
ALTER ROLE yourdbuser SET default_transaction_isolation TO 'read committed';
ALTER ROLE yourdbuser SET timezone TO 'UTC';
ALTER DATABASE yourdbname OWNER TO yourdbuser;

Common Pitfalls

  • Wrong host on Linux: if Postgres is configured to use peer authentication for local sockets, you may need to set DB_HOST=127.0.0.1 to force TCP and use password auth instead.
  • role does not exist: make sure you ran the CREATE USER command as a superuser.
  • Migrations work but the app still errors: confirm that the user has permissions on the schema (GRANT USAGE, CREATE ON SCHEMA public TO yourdbuser; in newer Postgres versions).
  • Connections piling up: in production, put a connection pooler like PgBouncer in front of Postgres, or use CONN_MAX_AGE in your Django settings to enable persistent connections.

Conclusion

You’ve now set up a PostgreSQL database named yourdbname, created a dedicated user yourdbuser with sensible defaults, and wired Django up to use it. From here you can start defining models, running migrations, and taking advantage of Postgres-specific features through django.contrib.postgres.

Want to go deeper on Postgres itself? Read PostgreSQL Fundamentals Every Backend Developer Should Know — full-text search, materialized views, indexing strategy, and the JSONB type are all worth your time as a Django developer.

Happy coding!


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .