Configuration and project structure

Organise config as classes, read secrets from the environment, keep machine-specific files in the instance folder, and make development and production differ only where they must.

Config objects

# app/config.py
import os
from datetime import timedelta

class Config:
    SECRET_KEY = os.environ.get("SECRET_KEY")      # None -> sessions break loudly
    SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL")
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    MAX_CONTENT_LENGTH = 5 * 1024 * 1024
    PERMANENT_SESSION_LIFETIME = timedelta(hours=8)

class DevelopmentConfig(Config):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = "sqlite:///" + os.path.join(os.getcwd(), "dev.db")

class TestingConfig(Config):
    TESTING = True
    DEBUG = False
    SECRET_KEY = "test-only-not-a-secret"
    SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"    # or a dedicated test database
    WTF_CSRF_ENABLED = False

class ProductionConfig(Config):
    DEBUG = False
    SESSION_COOKIE_SECURE = True
    PREFERRED_URL_SCHEME = "https"

CONFIGS = {
    "development": DevelopmentConfig,
    "testing": TestingConfig,
    "production": ProductionConfig,
}
# app/__init__.py
import os
from .config import CONFIGS

def create_app(config_object=None):
    app = Flask(__name__, instance_relative_config=True)
    name = config_object or os.environ.get("FLASK_CONFIG", "development")
    app.config.from_object(CONFIGS[name] if isinstance(name, str) else name)
    app.config.from_pyfile("config.py", silent=True)     # instance/config.py wins
    return app

# tests pass a class directly
app = create_app("testing")
KeyUsed forTypical value
SECRET_KEYSigning sessions and flash messagesLong random bytes from the environment
DEBUGReloader and interactive debuggerTrue in development only
TESTINGPropagates exceptions instead of returning 500True in the test config
SQLALCHEMY_DATABASE_URIDatabase connectionSQLite locally, PostgreSQL in production
MAX_CONTENT_LENGTHLargest accepted request bodyA few MB, matching the proxy limit
SESSION_COOKIE_SECURESend session cookie over HTTPS onlyTrue in production
PREFERRED_URL_SCHEMEScheme used by url_for(_external=True)https in production

Environment variables and the instance folder

# app/config.py — fail fast, in one place
import os

def required(name):
    try:
        return os.environ[name]
    except KeyError:
        raise RuntimeError(f"Missing required environment variable: {name}")

class ProductionConfig(Config):
    DEBUG = False
    SECRET_KEY = required("SECRET_KEY")
    SQLALCHEMY_DATABASE_URI = required("DATABASE_URL")

# local development: load a git-ignored .env before the app reads config
from dotenv import load_dotenv
load_dotenv()          # in run.py or wsgi.py, never in production images

# typed values: strings from the environment are always strings
DEBUG = os.environ.get("DEBUG", "").lower() in {"1", "true", "yes"}
MAX_CONTENT_LENGTH = int(os.environ.get("MAX_UPLOAD_MB", "5")) * 1024 * 1024
  • app.instance_path points at a directory outside the package where local, machine-specific files live: config.py with local overrides, an upload folder, a SQLite file. It is not copied into the installed package and not committed.
  • Later sources win: from_object then from_pyfile means the instance file overrides the class. Use that ordering deliberately so a developer can point at a local database without editing tracked code.
  • Give a secret no default in production. A missing SECRET_KEY that silently falls back to a string in the source means every deployment shares signing keys, and an attacker who reads the repository can forge session cookies.
  • Never commit a .env file. Provide a .env.example with placeholder values so a new checkout knows what to set.

Development, test and production

# tests/conftest.py
import pytest
from app import create_app
from app.extensions import db

@pytest.fixture
def app():
    application = create_app("testing")
    with application.app_context():
        db.create_all()
        yield application
        db.session.remove()
        db.drop_all()

@pytest.fixture
def client(app):
    return app.test_client()
export FLASK_CONFIG=production
export SECRET_KEY="$(python -c 'import secrets; print(secrets.token_hex(32))')"
export DATABASE_URL="postgresql+psycopg://app:pw@db:5432/app"
flask --app app:create_app run
⚠️
A configuration object with DEBUG = True written into tracked source is a production incident waiting for a copy-paste. The Werkzeug debugger executes arbitrary Python from a browser form, and its traceback pages expose source files, configuration values and environment variables. Keep DEBUG off in every class that is not DevelopmentConfig, drive the choice from the FLASK_CONFIG environment variable, and check the running value rather than the file you think was deployed.

FAQ

Where should the database URL come from?
The environment in production, a local default in development. Never hard-code credentials in a config class that is committed — the value ends up in the repository history, in image layers and in any error report that prints the config.
Why use config classes instead of app.config[...] assignments?
Classes give you a named, importable set of values, an obvious place to see the differences between environments, and something a test can pass into create_app(). Scattered assignments hide the defaults and make it impossible to build a second app with different settings.

Blueprints and the application factory Deployment: gunicorn, nginx and Docker

Last refreshed 2026-09-18.