Flask + PostgreSQL Production Setup
If you're trying to run Flask with PostgreSQL in production, this guide shows you how to configure the database, connect your app safely, and validate that the deployment works. Use it to replace SQLite or development settings with a production-ready PostgreSQL setup.
Quick Fix / Quick Setup
Use this for the fastest working path when PostgreSQL runs on the same host as Flask:
sudo -u postgres psql <<'SQL'
CREATE DATABASE flaskapp_prod;
CREATE USER flaskapp WITH PASSWORD 'change-this-password';
ALTER ROLE flaskapp SET client_encoding TO 'utf8';
ALTER ROLE flaskapp SET default_transaction_isolation TO 'read committed';
ALTER ROLE flaskapp SET timezone TO 'UTC';
GRANT ALL PRIVILEGES ON DATABASE flaskapp_prod TO flaskapp;
SQL
python -m venv .venv
source .venv/bin/activate
pip install flask gunicorn psycopg2-binary flask-sqlalchemy flask-migrate
export DATABASE_URL='postgresql://flaskapp:change-this-password@127.0.0.1:5432/flaskapp_prod'
flask db upgrade
python - <<'PY'
import os
from sqlalchemy import create_engine, text
engine = create_engine(os.environ['DATABASE_URL'])
with engine.connect() as conn:
print(conn.execute(text('SELECT 1')).scalar())
PY
Replace the password, move DATABASE_URL into a persistent environment file, and validate the same environment is loaded by systemd before sending traffic.
What’s Happening
PostgreSQL requires a valid connection URL, a real database user, correct privileges, and schema migrations that match the application code. Most production failures come from one of four places: invalid DATABASE_URL, authentication problems, missing PostgreSQL driver, or migrations not applied. If Flask starts but fails on requests, check both the app service environment and PostgreSQL logs.
Step-by-Step Guide
- Install PostgreSQL server and client packagesbash
sudo apt update sudo apt install -y postgresql postgresql-contrib libpq-dev - Enable and start PostgreSQLbash
sudo systemctl enable --now postgresql sudo systemctl status postgresql - Create a production database and dedicated database userbash
sudo -u postgres psql <<'SQL' CREATE DATABASE flaskapp_prod; CREATE USER flaskapp WITH PASSWORD 'strong-password'; ALTER ROLE flaskapp SET client_encoding TO 'utf8'; ALTER ROLE flaskapp SET default_transaction_isolation TO 'read committed'; ALTER ROLE flaskapp SET timezone TO 'UTC'; GRANT ALL PRIVILEGES ON DATABASE flaskapp_prod TO flaskapp; SQL - If schema creation fails later, grant schema privileges
Some deployments also require explicit access to thepublicschema:bashsudo -u postgres psql -d flaskapp_prod <<'SQL' GRANT ALL ON SCHEMA public TO flaskapp; ALTER SCHEMA public OWNER TO flaskapp; SQL - Create or activate the application virtual environmentbash
python -m venv .venv source .venv/bin/activate - Install Flask and PostgreSQL dependenciesbash
pip install flask gunicorn psycopg2-binary flask-sqlalchemy flask-migrate - Set the production database URLbash
export DATABASE_URL='postgresql://flaskapp:strong-password@127.0.0.1:5432/flaskapp_prod' - Configure Flask to read the database URL from the environment
Example configuration:pythonimport os SQLALCHEMY_DATABASE_URI = os.environ["DATABASE_URL"] SQLALCHEMY_TRACK_MODIFICATIONS = False
If using an app factory:pythonimport os from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate db = SQLAlchemy() migrate = Migrate() def create_app(): app = Flask(__name__) app.config["SQLALCHEMY_DATABASE_URI"] = os.environ["DATABASE_URL"] app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False db.init_app(app) migrate.init_app(app, db) return app - Set the Flask app target for CLI commands if requiredbash
export FLASK_APP='wsgi.py'
Or for a factory-based app:bashexport FLASK_APP='myapp:create_app()' - Initialize migrations if the project does not already include them
flask db init
- Generate and apply schema migrations
flask db migrate -m 'initial'
flask db upgrade
- Test direct database connectivity from the same environment
python -c "import os, psycopg2; conn = psycopg2.connect(os.environ['DATABASE_URL']); print('ok'); conn.close()"
Or with SQLAlchemy:
python - <<'PY'
import os
from sqlalchemy import create_engine, text
engine = create_engine(os.environ['DATABASE_URL'])
with engine.connect() as conn:
print(conn.execute(text('SELECT current_database(), current_user')).fetchone())
PY
- Move secrets into a persistent environment file for systemd
sudo mkdir -p /etc/flask
sudo tee /etc/flask/myapp.env > /dev/null <<'EOF'
DATABASE_URL=postgresql://flaskapp:strong-password@127.0.0.1:5432/flaskapp_prod
FLASK_ENV=production
EOF
sudo chmod 600 /etc/flask/myapp.env
- Reference the environment file in the Gunicorn systemd unit
Example unit:
[Unit]
Description=Gunicorn instance for myapp
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/myapp
EnvironmentFile=/etc/flask/myapp.env
ExecStart=/var/www/myapp/.venv/bin/gunicorn --workers 3 --bind unix:/run/myapp.sock wsgi:app
[Install]
WantedBy=multi-user.target
- Reload systemd and restart the application service
sudo systemctl daemon-reload
sudo systemctl restart myapp
sudo systemctl status myapp
- Validate requests through Gunicorn and Nginx
Load a route that touches the database, such as a login route, health endpoint, or model query route. If you have not set up Gunicorn and Nginx yet, use the main deployment flow in Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide).
- Validate PostgreSQL access with
psqlusing the same credentials
psql 'postgresql://flaskapp:strong-password@127.0.0.1:5432/flaskapp_prod' -c 'SELECT current_database(), current_user;'
Common Causes
- Wrong connection string format → Flask cannot connect or parses invalid settings → use
postgresql://user:password@host:5432/dbnameand verify the exact value loaded by systemd. - PostgreSQL user lacks privileges → migrations or table creation fail → grant database privileges and schema privileges on
public. - Missing PostgreSQL driver → app crashes on import → install
psycopg2-binaryor another supported PostgreSQL driver in the same virtual environment Gunicorn uses. - Migrations not applied → app starts but fails on first query because tables are missing → run
flask db upgradein the production environment. - Local socket vs TCP mismatch → app tries a socket path while PostgreSQL expects TCP or vice versa → explicitly use
127.0.0.1:5432inDATABASE_URL. - Environment variables not loaded under systemd → app works in shell but fails as a service → define
EnvironmentFile=orEnvironment=in the service unit and restart it. - Authentication method mismatch in
pg_hba.conf→ password auth fails even with correct credentials → reviewpg_hba.confand reload PostgreSQL. - Firewall or remote-host restrictions → connection timeouts to a managed PostgreSQL instance → allow the host or network and verify listen and bind settings.
Debugging Section
Check the application service:
sudo journalctl -u myapp -n 200 --no-pager
sudo journalctl -u myapp -f
sudo systemctl cat myapp
sudo systemctl status myapp
Check PostgreSQL:
sudo systemctl status postgresql
sudo tail -n 200 /var/log/postgresql/postgresql-*.log
sudo ss -ltnp | grep 5432
Inspect roles and databases:
sudo -u postgres psql -c '\du'
sudo -u postgres psql -c '\l'
Test database login directly:
psql 'postgresql://flaskapp:strong-password@127.0.0.1:5432/flaskapp_prod' -c 'SELECT current_database(), current_user;'
Test database access from the application environment:
source .venv/bin/activate
python -c "import os, psycopg2; conn = psycopg2.connect(os.environ['DATABASE_URL']); print('ok'); conn.close()"
flask db upgrade
What to look for:
password authentication faileddatabase does not existrelation does not existcould not connect to serverModuleNotFoundErrorpermission denied for schema public
If shell tests pass but the service fails, compare:
- service user
- virtual environment path
WorkingDirectoryEnvironmentFile- loaded
DATABASE_URL
For environment loading and secret handling, see Flask Environment Variables and Secrets Setup. For direct connection failures, see Flask Database Connection Errors in Production.
Checklist
- PostgreSQL is installed, enabled, and running
- A dedicated production database exists
- A dedicated database user exists with the correct password
-
DATABASE_URLpoints to the correct host, port, user, and database - The PostgreSQL driver is installed in the active virtual environment
- Flask reads the database URL from environment variables
- Migrations complete successfully with
flask db upgrade - Gunicorn and systemd load the same environment used in manual tests
- Application routes that query the database return successful responses
- PostgreSQL and application logs show no authentication or schema errors after restart
For broader production validation, use Flask Production Checklist (Everything You Must Do).
Related Guides
- Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide)
- Flask Environment Variables and Secrets Setup
- Flask Database Connection Errors in Production
- Flask Migrations Not Applied (Fix Guide)
- Flask Production Checklist (Everything You Must Do)
FAQ
Q: Should production Flask use SQLite?
No. Use PostgreSQL or another production database for concurrency, durability, and operational safety.
Q: Should I use psycopg2-binary in production?
It works for many deployments, but some teams prefer source-built drivers or psycopg3 packages for tighter control.
Q: Where should DATABASE_URL be stored?
In a systemd environment file, secret manager, or deployment platform configuration, not hardcoded in the repository.
Q: Do I need migrations if tables already exist?
Yes, if your app uses Alembic or Flask-Migrate for schema control. Migration state must match the real database before future deploys.
Q: Can PostgreSQL run on another host?
Yes. Update the host in DATABASE_URL, allow network access, and secure credentials, SSL settings, and firewall rules as required.
Final Takeaway
A production Flask + PostgreSQL setup depends on four things: a correct DATABASE_URL, an installed driver, applied migrations, and systemd or Gunicorn loading the same environment you tested manually. If shell tests pass but the app fails, check service environment loading, PostgreSQL authentication, and schema state first.