Connect Django to Managed PostgreSQL Securely
Moving a Django app from a local database or self-managed PostgreSQL server to a managed PostgreSQL service sounds simple: update the host, username, and password, then deploy.
Problem statement
Moving a Django app from a local database or self-managed PostgreSQL server to a managed PostgreSQL service sounds simple: update the host, username, and password, then deploy. In production, that is not enough.
The real deployment problem is making the switch without hardcoding credentials, disabling SSL, exposing the database to the internet, or running migrations against the wrong target. A bad cutover can leave the app unable to connect, partially migrated, or pointed at the wrong database with no clean rollback path.
If you want a safe Django-to-managed-PostgreSQL setup, treat the database change as a production deployment task: secrets in environment variables, TLS enforced, network access restricted, connectivity verified before migration, and previous settings preserved for rollback.
Quick answer
To connect Django to managed PostgreSQL securely in production:
- Store database credentials in environment variables or a secrets manager.
- Configure Django with the PostgreSQL backend.
- Enforce TLS with
sslmode=require, or useverify-fullif your provider supports certificate verification. - Restrict database access to only your app runtime.
- Test the connection from the application environment before running
migrate. - Take a backup or snapshot before cutover.
- Keep the previous database configuration available so you can revert quickly if the new connection fails.
- Do not switch app traffic until connectivity checks, secret injection, and your migration plan are confirmed.
Step-by-step solution
1) Define what a secure managed PostgreSQL connection looks like
Your production database connection should meet these goals:
- credentials are not committed to Git
- traffic between Django and PostgreSQL is encrypted
- only expected application hosts can reach the database
- the app can connect before traffic is switched
- migrations are run intentionally, with backup and rollback planning
Common mistakes to avoid:
- hardcoding the password in
settings.py - allowing
0.0.0.0/0or broad IP ranges unless you have no other option - setting
sslmode=disable - testing directly against production first
- changing the database target without a backup or previous config snapshot
This guide covers the database side only. You still need normal production settings such as correct DEBUG, ALLOWED_HOSTS, HTTPS, and process restarts when secrets change.
2) Gather the managed PostgreSQL connection details
From your provider dashboard, collect:
- host
- port
- database name
- username
- password
- SSL requirement details
- CA certificate path or download, if your provider requires it
Some providers give a full connection string. Others give separate values. Both work.
If you prefer explicit Django settings, use separate environment variables:
export DB_NAME=appdb
export DB_USER=appuser
export DB_PASSWORD='strong-password'
export DB_HOST=db.example-provider.com
export DB_PORT=5432
export DB_SSLMODE=require
If your provider requires certificate verification, also set:
export DB_SSLROOTCERT=/etc/ssl/certs/provider-ca.pem
If you use a single connection string, keep it in a secret value such as:
export DATABASE_URL='postgresql://appuser:password@db.example-provider.com:5432/appdb?sslmode=require'
Verification check:
- confirm the hostname, port, and database name match the intended environment
- confirm the DNS name resolves from the application runtime
- confirm the network path is open from the app host or container, not just from your laptop
Do not point staging at production by accident.
3) Install the PostgreSQL driver Django will use
Django needs a PostgreSQL client library. Use either psycopg (psycopg 3) or psycopg2. Pin the version in your dependency file for reproducible deploys.
pip install "psycopg[binary]"
# or, if your project already uses psycopg2 and you build system dependencies:
pip install psycopg2
For many production builds, psycopg is a good default. If your project already uses psycopg2, keep it consistent unless you have a reason to switch, but avoid treating psycopg2-binary as a production default.
Verification check:
python -c "import psycopg; print('psycopg ok')"
# or
python -c "import psycopg2; print('psycopg2 ok')"
Rollback note: if the deploy fails because the driver is missing in the runtime image or virtualenv, revert to the previous build artifact or dependency lock before changing database settings.
4) Configure Django database settings with environment variables
Example using explicit DATABASES settings
This is the most transparent option for a Django PostgreSQL production configuration:
import os
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ["DB_NAME"],
"USER": os.environ["DB_USER"],
"PASSWORD": os.environ["DB_PASSWORD"],
"HOST": os.environ["DB_HOST"],
"PORT": os.environ.get("DB_PORT", "5432"),
"CONN_MAX_AGE": int(os.environ.get("DB_CONN_MAX_AGE", "60")),
"OPTIONS": {
"sslmode": os.environ.get("DB_SSLMODE", "require"),
},
}
}
If your provider requires a CA certificate for hostname verification:
sslrootcert = os.environ.get("DB_SSLROOTCERT")
if sslrootcert:
DATABASES["default"]["OPTIONS"]["sslrootcert"] = sslrootcert
Example using DATABASE_URL
If your project already uses URL-based config, that is also fine, but keep SSL options in the URL and keep the whole value in a secret store.
Keep secrets out of source control
Do not commit any of these values:
.envfiles with production credentials- raw passwords in
settings.py - copied provider connection strings in docs or examples checked into the repo
Use your platform’s secret injection mechanism, environment variables, or a dedicated secrets manager.
Safe defaults for production
At minimum:
ENGINEmust bedjango.db.backends.postgresqlsslmodeshould default torequireCONN_MAX_AGEshould be set deliberately, not left as an accident of old settings
Verification check:
python manage.py check --deploy
This does not validate the database connection itself, but it helps catch nearby production setting issues.
5) Enforce SSL/TLS for the Django-to-PostgreSQL connection
For most managed database providers, sslmode=require is the minimum acceptable setting for encryption in transit, but it does not verify the server certificate or hostname.
"OPTIONS": {
"sslmode": "require",
}
If your provider supports certificate validation and gives you a CA bundle, use a stricter mode:
"OPTIONS": {
"sslmode": "verify-full",
"sslrootcert": "/etc/ssl/certs/provider-ca.pem",
}
require encrypts the connection but does not validate server identity. verify-full encrypts the connection and verifies the server certificate and hostname. Use verify-full when your provider documentation supports it and you can place the CA certificate on the app host or container image securely.
Provider-specific note: certificate requirements vary. Some providers document require as the expected setting. Others support or require CA verification. Follow the provider documentation rather than guessing.
Verification check after deploy:
- confirm the CA file exists if configured
- inspect provider dashboards for encrypted sessions if available
- verify from PostgreSQL that SSL is active:
SELECT ssl, version, cipher
FROM pg_stat_ssl
WHERE pid = pg_backend_pid();
You can run that through python manage.py dbshell only if the PostgreSQL client (psql) is installed in that environment, or execute it through a cursor in Django shell.
6) Restrict database access at the network layer
Even with SSL enabled, the database should not be broadly reachable.
Prefer:
- private networking between app and database, if your platform supports it
- narrow IP allowlists for app servers or platform egress IPs
- separate access rules for staging and production
If your app runs from dynamic IPs, check whether your platform offers:
- fixed egress IPs
- private service networking
- a managed connection proxy
Avoid opening the database to all internet sources just to make deployment easier.
Verification check:
- confirm only expected source networks are allowed
- test from the application environment, not your laptop only
- confirm that rejected sources cannot connect
Rollback note: if the app cannot connect after cutover, check firewall and allowlist rules before changing Django settings again.
7) Test the connection before running migrations
Before migrate, prove that the app can authenticate and execute queries.
Run basic checks:
python manage.py showmigrations
python manage.py shell
In the Django shell:
from django.db import connection
with connection.cursor() as cursor:
cursor.execute("SELECT current_database(), current_user;")
print(cursor.fetchone())
If needed, also confirm that the configured host resolves from inside the running app container or VM and that the process has loaded the new secret values after restart or redeploy.
If you have a safe noncritical model or a staging environment, test a simple read and write path there before production cutover.
Verification checklist:
- Django starts with the new settings
- a simple SQL query succeeds
- the query runs against the expected database
- SSL is enabled for the session
- the application process is using the updated secret values
Do not run migrations until these checks pass.
8) Apply migrations safely in production
Before changing schema:
- take a provider snapshot or manual backup
- export or version the current secret/config values
- confirm the app is pointed at the managed database you intend to migrate
- confirm whether this target already contains restored application data or is a fresh database
Then run:
python manage.py migrate
After migration, check:
python manage.py showmigrations
Minimal rollback rule:
- if the app has not started serving production writes to the new database, rollback can usually be a config revert plus redeploy
- if production writes have already started on the new database, rollback is a recovery decision and may require data reconciliation
If a migration fails, do not continue with partial cutover. Stop and assess:
- did the failure happen before any schema changes?
- were some migrations applied?
- is the app already sending writes to the new database?
A backup is not the same as an instant rollback. Once writes diverge between old and new databases, reverting safely becomes much harder.
9) Verify the application after deployment
After the deploy:
- check application logs for connection errors
- hit health endpoints
- confirm normal page loads or API responses
- test at least one read and one write path
- watch provider session counts and connection limits
If you use PgBouncer or another pooler, confirm your connection settings are compatible with it. Managed databases often have lower connection limits than self-hosted installs, so verify that Gunicorn worker count, Django connection reuse, and pooler settings make sense together.
Keep the previous database secret values available until you are confident the new deployment is stable.
Explanation
This setup works because it separates concerns cleanly:
- secrets are injected at runtime rather than stored in code
- PostgreSQL settings are explicit and reproducible
- TLS is enforced at the client level
- network restrictions reduce exposure if credentials leak
- migrations happen only after connectivity is validated
- rollback remains possible because previous settings are preserved and traffic is not switched too early
Use explicit DATABASES settings when you want clarity and easier per-option control. Use DATABASE_URL when your platform standardizes on connection strings. Both are valid if secrets are handled correctly and SSL options are present.
When to turn this into a reusable script or template
If you deploy multiple environments or repeat this process often, the manual steps become easy to miss. Good candidates for automation are secret validation, SSL option checks, pre-migration connectivity tests, backup reminders, and post-deploy smoke tests. A reusable settings template and CI/CD preflight step usually provide the biggest reliability gain first.
Notes and edge cases
- PgBouncer or provider poolers: transaction pooling can affect features like server-side prepared statements and long-lived connections. Test with your exact provider mode.
CONN_MAX_AGE: a small nonzero value such as60is reasonable for many apps, but review this if you use aggressive pooling or serverless runtimes.- Containers and CI/CD: secret injection differs between Docker Compose, Kubernetes, PaaS platforms, and VM-based deploys. Keep the Django settings the same; only the secret delivery mechanism should change.
- Process restarts: changing environment variables does nothing until the app process is restarted or the deployment is rolled out again.
- Multiple environments: use separate databases and separate credentials for staging and production. Do not share one managed database unless you are intentionally isolating by schema and understand the risk.
- Client certificates: some providers or enterprise setups may require client certificate authentication in addition to CA verification. Follow provider documentation if
sslcertandsslkeyare required. - Related production concerns: database cutover is only one part of deployment. You still need correct static files handling, reverse proxy configuration, HTTPS, and safe app server restarts.
Internal links
For the secret management side of this setup, see Django environment variables in production.
For a broader production readiness review, see Django Deployment Checklist for Production.
For the web stack around your app, see Deploy Django with Gunicorn and Nginx on Ubuntu and Deploy Django ASGI with Uvicorn and Nginx.
For migration workflow details, see run Django migrations safely in production.
If the app still cannot connect, use troubleshoot Django database connection errors in production.
FAQ
Do I need sslmode=require for managed PostgreSQL?
Usually yes. For a production Django-to-PostgreSQL connection, sslmode=require is a practical minimum for encryption, though verify-full is stronger when supported.
What is the difference between require and verify-full?
require encrypts traffic between Django and PostgreSQL, but it does not verify the database server identity. verify-full encrypts traffic and verifies the certificate and hostname using the CA file you provide.
Should I use DATABASE_URL or explicit Django settings?
Either is fine. Explicit settings are easier to read and audit in Django. DATABASE_URL is convenient on platforms that already inject connection strings. The important part is that secrets stay out of Git and SSL options are not omitted.
How do I rotate database credentials without downtime?
Create or stage the new credential in your provider first, update the runtime secret, and redeploy so new connections use it. Keep the old credential valid briefly if your provider supports overlap. Then remove the old credential after you confirm the app is stable.
Can I point staging and production to the same managed database?
No, not as a normal practice. Use separate databases and separate credentials. Sharing a database increases the risk of accidental schema changes, test data contamination, and production outages.