Streamlining Configuration Across Environments in Django and Flask
Emily Parker
Product Engineer · Leapcell

Introduction
Developing robust web applications often involves navigating a crucial challenge: managing diverse configurations across different stages of the software lifecycle. What works perfectly fine in a developer's local environment—for instance, connecting to a SQLite database or enabling verbose debugging—would be disastrous in a production setting. In production, we need highly optimized database connections, robust logging, strict security settings, and minimal debugging information exposed to the public. Similarly, testing environments demand a specific set of configurations to ensure isolated and repeatable tests. Ignoring these differences leads to frustrating bugs, security vulnerabilities, and deployment headaches. This article explores practical and effective strategies for gracefully handling these distinct configurations in Python's popular web frameworks, Django and Flask, ensuring a smooth transition from development to production.
Core Concepts and Implementation Strategies
Before diving into the specifics of Django and Flask, let's establish some core concepts that are fundamental to effective configuration management.
Core Terminology
- Environment Variables: These are dynamic-named values that can affect the way running processes will behave on a computer. They are commonly used to store sensitive information (like API keys, database credentials) or environment-specific settings without hardcoding them into the application's codebase.
- Settings/Configuration Files: These files (often
settings.py
in Django or custom.py
or.ini
files in Flask) contain key-value pairs that dictate an application's behavior, such as database connections, enabled apps, middleware, and logging levels. SECRET_KEY
: A critical security setting in Django (and often used in Flask for session management) that is used for cryptographic signing. It must be kept secret and unique for each environment, especially production.- Debugging Status: A boolean flag (
DEBUG
in Django,DEBUG
in Flask) that controls whether debugging information is displayed. It should always beFalse
in production.
General Principles for Configuration
- Don't commit secrets to version control: Sensitive information like database passwords, API keys, and
SECRET_KEY
should never be hardcoded or committed to Git. - Separate environment-specific settings: Configurations that vary between development, testing, and production should be decoupled from the core application logic.
- Prioritize environment variables for sensitive data: Environment variables provide a secure and flexible way to inject sensitive data into your application at runtime.
- Use sensible defaults: Provide default configurations that work well for development, and then override them for other environments.
Django Configuration Strategy
Django's built-in settings management is powerful but requires careful structuring for multi-environment deployments. A common and highly recommended approach is to separate settings into a package.
Let's assume a project structure like this:
myproject/
├── manage.py
├── myproject/
│ ├── __init__.py
│ ├── urls.py
│ ├── wsgi.py
│ └── settings/
│ ├── __init__.py
│ ├── base.py
│ ├── development.py
│ ├── production.py
│ └── test.py
└── myapp/
└── ...
1. myproject/settings/base.py
:
This file contains all common settings that apply to all environments.
# myproject/settings/base.py import os from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent.parent # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! # Use environment variable for SECRET_KEY SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'default-insecure-key-for-dev-only') # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False # Default to False, overridden in development.py ALLOWED_HOSTS = [] # Overridden in specific environments # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'myapp', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] ROOT_URLCONF = 'myproject.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 'myproject.wsgi.application' DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', } } AUTH_PASSWORD_VALIDATORS = [ # ... default validators ... ] LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_TZ = True STATIC_URL = '/static/' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
2. myproject/settings/development.py
:
This file imports base.py
and overrides settings for local development.
# myproject/settings/development.py from .base import * DEBUG = True ALLOWED_HOSTS = ['127.0.0.1', 'localhost'] # Use a simpler database for development DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', } } # Add any development-specific apps or tools INSTALLED_APPS += [ 'debug_toolbar', ] MIDDLEWARE += [ 'debug_toolbar.middleware.DebugToolbarMiddleware', ] # Django Debug Toolbar configuration INTERNAL_IPS = [ '127.0.0.1', ] LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'handlers': { 'console': { 'class': 'logging.StreamHandler', }, }, 'loggers': { 'django': { 'handlers': ['console'], 'level': 'INFO', }, 'myapp': { 'handlers': ['console'], 'level': 'DEBUG', # More verbose logging for your app in dev }, }, }
3. myproject/settings/production.py
:
This file imports base.py
and sets up production-ready configurations.
# myproject/settings/production.py from .base import * DEBUG = False # Load SECRET_KEY from environment variable for security SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY') if not SECRET_KEY: raise ImproperlyConfigured('DJANGO_SECRET_KEY environment variable not set') ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', '').split(',') if not ALLOWED_HOSTS: raise ImproperlyConfigured('DJANGO_ALLOWED_HOSTS environment variable not set') # Production database settings (e.g., PostgreSQL) DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': os.environ.get('POSTGRES_DB_NAME'), 'USER': os.environ.get('POSTGRES_DB_USER'), 'PASSWORD': os.environ.get('POSTGRES_DB_PASSWORD'), 'HOST': os.environ.get('POSTGRES_DB_HOST'), 'PORT': os.environ.get('POSTGRES_DB_PORT', '5432'), } } # Static file storage and serving STATIC_ROOT = BASE_DIR / 'staticfiles' STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' # Recommended for production # Security settings CSRF_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True SECURE_SSL_REDIRECT = True SECURE_HSTS_SECONDS = 31536000 # 1 year SECURE_HSTS_INCLUDE_SUBDOMAINS = True SECURE_HSTS_PRELOAD = True SECURE_BROWSER_XSS_FILTER = True X_FRAME_OPTIONS = 'DENY' # Logging for production (e.g., to file or external service) LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'verbose': { 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}', 'style': '{', }, }, 'handlers': { 'file': { 'level': 'INFO', 'class': 'logging.handlers.RotatingFileHandler', 'filename': '/var/log/myproject/myproject.log', # Adjust path as needed 'maxBytes': 1024*1024*5, # 5MB 'backupCount': 5, 'formatter': 'verbose', }, 'console': { # Still useful for containerized environments 'class': 'logging.StreamHandler', 'formatter': 'verbose', }, }, 'loggers': { 'django': { 'handlers': ['file', 'console'], 'level': 'INFO', 'propagate': True, }, 'myapp': { 'handlers': ['file', 'console'], 'level': 'WARNING', # Less verbose for production unless critical 'propagate': False, }, }, }
4. myproject/settings/test.py
:
Similar to development, but with very specific changes for isolated testing.
# myproject/settings/test.py from .base import * DEBUG = False # Tests should run as close to production as possible, without debug info # Use an in-memory SQLite database for fast, isolated tests DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:', } } # Speed up password hashing during tests PASSWORD_HASHERS = [ 'django.contrib.auth.hashers.MD5PasswordHasher', ] SECRET_KEY = 'test-insecure-key' # Safe for test
5. How to Load the Correct Settings:
Django loads settings using the DJANGO_SETTINGS_MODULE
environment variable.
- Development:
export DJANGO_SETTINGS_MODULE=myproject.settings.development
- Production:
export DJANGO_SETTINGS_MODULE=myproject.settings.production
- Testing:
export DJANGO_SETTINGS_MODULE=myproject.settings.test
You can set this in your web server configuration (e.g., Gunicorn, uWSGI) or directly when running Django commands:
# For development DJANGO_SETTINGS_MODULE=myproject.settings.development python manage.py runserver # For production (via Gunicorn example) DJANGO_SETTINGS_MODULE=myproject.settings.production gunicorn myproject.wsgi:application
Flask Configuration Strategy
Flask provides flexibility in how configurations are managed. A common approach leverages Python classes and environment variables.
Let's assume a project structure:
myflaskapp/
├── run.py
├── config.py
└── myflaskapp/
├── __init__.py
└── views.py
1. config.py
:
This file defines base, development, testing, and production configuration classes.
# config.py import os from datetime import timedelta class Config: """Base configuration settings.""" SECRET_KEY = os.environ.get('FLASK_SECRET_KEY', 'dev-secret-key-insecure') SQLALCHEMY_TRACK_MODIFICATIONS = False PERMANENT_SESSION_LIFETIME = timedelta(days=7) LOG_LEVEL = 'INFO' class DevelopmentConfig(Config): """Development configuration.""" DEBUG = True ENV = 'development' SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \ 'sqlite:///' + os.path.join(os.path.abspath(os.path.dirname(__file__)), 'dev.db') LOG_LEVEL = 'DEBUG' class TestingConfig(Config): """Testing configuration.""" DEBUG = True # Keep debug true for detailed error reporting during tests ENV = 'testing' TESTING = True # Flask-specific flag SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:' # In-memory database for tests LOG_LEVEL = 'WARNING' SECRET_KEY = 'test-secret-key' # Safe for test class ProductionConfig(Config): """Production configuration.""" DEBUG = False ENV = 'production' SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') if SQLALCHEMY_DATABASE_URI is None: raise ValueError("DATABASE_URL environment variable not set for production.") # Override SECRET_KEY from environment for production SECRET_KEY = os.environ.get('FLASK_SECRET_KEY') if SECRET_KEY is None: raise ValueError("FLASK_SECRET_KEY environment variable not set for production.") # Logging: For production, typically send to file or a log management service LOG_LEVEL = 'ERROR' # Only high-severity logs
2. myflaskapp/__init__.py
:
This file initializes the Flask app and loads the appropriate configuration.
# myflaskapp/__init__.py from flask import Flask import os import logging from logging.handlers import RotatingFileHandler def create_app(): app = Flask(__name__) # Determine which config to load based on FLASK_ENV environment variable # It's common to default to DevelopmentConfig if FLASK_ENV is not set config_name = os.environ.get('FLASK_ENV', 'development') if config_name == 'production': app.config.from_object('config.ProductionConfig') elif config_name == 'testing': app.config.from_object('config.TestingConfig') else: # Default to development app.config.from_object('config.DevelopmentConfig') # Example of integrating a database if you're using one like SQLAlchemy # from flask_sqlalchemy import SQLAlchemy # db = SQLAlchemy(app) # Configure logging based on the environment if not app.debug and not app.testing: # Production logging example (to a file) if not os.path.exists('logs'): os.mkdir('logs') file_handler = RotatingFileHandler('logs/myflaskapp.log', maxBytes=10240, backupCount=10) file_handler.setFormatter(logging.Formatter( '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]' )) file_handler.setLevel(logging.ERROR) # Use app.config.get('LOG_LEVEL', 'ERROR') app.logger.addHandler(file_handler) app.logger.setLevel(logging.INFO) # Use app.config.get('LOG_LEVEL', 'INFO') app.logger.info('MyFlaskApp startup') elif app.debug: # Development logging to console app.logger.setLevel(logging.DEBUG) app.logger.info('MyFlaskApp development startup') from . import views app.register_blueprint(views.bp) return app
3. run.py
(or similar entry point):
# run.py from myflaskapp import create_app app = create_app() if __name__ == '__main__': app.run()
4. How to Load the Correct Settings:
Flask conventionally uses the FLASK_ENV
environment variable, which can be development
, production
, or testing
.
- Development:
export FLASK_ENV=development
- Production:
export FLASK_ENV=production
- Testing:
export FLASK_ENV=testing
Then run your application:
# For development export FLASK_ENV=development python run.py # For production (via Gunicorn example) export FLASK_ENV=production export DATABASE_URL="postgresql://user:password@host/dbname" export FLASK_SECRET_KEY="a-very-long-and-secure-random-string" gunicorn 'myflaskapp:create_app()'
Managing Environment Variables
Regardless of whether you use Django or Flask, securely managing environment variables is paramount.
-
For Development: Use a
.env
file and a library likepython-dotenv
.pip install python-dotenv
- Create a
.env
file in your project root (e.g.,myproject/.env
ormyflaskapp/.env
):DJANGO_SECRET_KEY='your-dev-secret-key-here' POSTGRES_DB_NAME='myproject_dev' FLASK_SECRET_KEY='your-dev-secret-key-here' DEV_DATABASE_URL='sqlite:///dev.db'
- Add
.env
to your.gitignore
file. - Load it in your
settings.py
(Django) orconfig.py
/__init__.py
(Flask):# In Django settings/base.py or Flask config.py/init.py from dotenv import load_dotenv load_dotenv() # This loads variables from .env into os.environ # Then access them as usual SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
-
For Production: Environment variables are typically set by your hosting provider (e.g., Heroku config vars, Kubernetes secrets, AWS EC2 environment variables, Docker compose
env_file
). Do not use.env
files in production. They are for local convenience.
Application Scenarios
The configurations outlined above are suitable for a wide range of scenarios:
- Local Development: Fast, iterative changes with verbose logging and easy-to-use local databases.
- Automated Testing: Isolated, repeatable tests using in-memory or temporary databases to ensure speed and prevent side effects.
- Staging/Pre-production: A near-identical replica of production, using external databases and services, allowing final checks before deployment.
- Production: Secure, optimized, and performant setup with external services, robust monitoring, and minimal debugging exposure.
Conclusion
Effectively managing configurations across different environments is a cornerstone of building reliable and secure web applications with Django and Flask. By separating environment-specific settings, leveraging environment variables for sensitive data, and carefully structuring your configuration files, you establish a robust and maintainable system. A well-organized configuration strategy drastically reduces deployment risks and enhances application stability.