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 postgresqlon macOS,sudo apt install postgresqlon Debian/Ubuntu). - A working Python 3.9+ environment.
- A Django project (or one you can spin up with
django-admin startproject). - The
psycopg2-binarydriver installed:pip install psycopg2-binary. For production builds you’d typically preferpsycopg2(compiled from source) so you can match the system’s libpq version, butpsycopg2-binaryis 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
peerauthentication for local sockets, you may need to setDB_HOST=127.0.0.1to force TCP and use password auth instead. role does not exist: make sure you ran theCREATE USERcommand 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_AGEin 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 .