Settings Structure#
Organization of Django settings for different environments.
Overview#
Construbot uses environment-based settings with inheritance to avoid duplication and support multiple environments.
Location: construbot/config/settings/
Files:
base.py- Shared settings for all environmentslocal.py- Development settingstest.py- Testing settingsproduction.py- Production settings
Settings Hierarchy#
base.py (Common settings)
├── local.py (Development)
├── test.py (Testing)
└── production.py (Production)
All environment files import from base.py:
# local.py, test.py, production.py
from .base import * # Import all base settings
from .base import env # Import environ instance
# Override specific settings
DEBUG = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
Base Settings (base.py)#
Common settings shared across all environments:
Core Django:
# Django version
DJANGO_ADMIN_URL = env('DJANGO_ADMIN_URL', default='admin/')
# Internationalization
LANGUAGE_CODE = 'es-mx'
TIME_ZONE = 'America/Mexico_City'
USE_I18N = True
USE_L10N = True
USE_TZ = True
Installed Apps:
INSTALLED_APPS = [
# AutocompleteLight MUST be before admin
'dal',
'dal_select2',
# Django contrib
'django.contrib.admin',
'django.contrib.auth',
...
# Third-party
'rest_framework',
'rest_framework.authtoken',
'allauth',
'allauth.account',
...
# Local apps
'construbot.users',
'construbot.proyectos',
'construbot.api',
'construbot.core',
]
Warning
dal and dal_select2 MUST be listed before django.contrib.admin or autocomplete widgets won’t work.
Middleware:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', # Static files
'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',
]
Authentication:
AUTH_USER_MODEL = 'users.User' # Custom user model
AUTHENTICATION_BACKENDS = [
'construbot.core.backends.ModelBackend', # Custom backend
'allauth.account.auth_backends.AuthenticationBackend',
]
Database (uses DATABASE_URL):
DATABASES = {
'default': env.db('DATABASE_URL'),
}
DATABASES['default']['ATOMIC_REQUESTS'] = True
Static/Media Files:
STATIC_ROOT = str(ROOT_DIR / 'staticfiles')
STATIC_URL = '/static/'
STATICFILES_DIRS = [str(APPS_DIR / 'static')]
MEDIA_ROOT = str(APPS_DIR / 'media')
MEDIA_URL = '/media/'
Password Hashers:
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.Argon2PasswordHasher', # Primary
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
]
Celery:
if USE_TZ:
CELERY_TIMEZONE = TIME_ZONE
CELERY_BROKER_URL = env('CELERY_BROKER_URL', default=env('REDIS_URL', default='redis://localhost:6379/0'))
CELERY_RESULT_BACKEND = CELERY_BROKER_URL
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TASK_TIME_LIMIT = 5 * 60 # 5 minutes
CELERY_TASK_SOFT_TIME_LIMIT = 60 # 1 minute
Local Settings (local.py)#
Development environment settings:
Debug Mode:
DEBUG = True
Allowed Hosts:
ALLOWED_HOSTS = ['localhost', '0.0.0.0', '127.0.0.1']
Database:
Can use environment variable or defaults to local PostgreSQL:
DATABASES = {
'default': env.db('DATABASE_URL', default='postgresql://debug:debug@postgres:5432/construbot')
}
Caching (development - dummy cache):
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': '',
}
}
Email (console backend):
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
EMAIL_HOST = 'localhost'
EMAIL_PORT = 1025 # MailHog
Debugging Tools:
INSTALLED_APPS += ['debug_toolbar']
MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware']
INTERNAL_IPS = ['127.0.0.1', '10.0.2.2']
django-extensions:
INSTALLED_APPS += ['django_extensions']
Test Settings (test.py)#
Testing environment settings:
Debug OFF:
DEBUG = False
Database (in-memory SQLite):
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
}
Fast Password Hashing:
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.MD5PasswordHasher',
]
Templates (no caching):
TEMPLATES[0]['OPTIONS']['loaders'] = [
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
]
Email (memory backend):
EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
Celery (synchronous execution):
CELERY_TASK_ALWAYS_EAGER = True
CELERY_TASK_EAGER_PROPAGATES = True
Production Settings (production.py)#
Production environment settings:
Debug OFF:
DEBUG = env.bool('DJANGO_DEBUG', False)
Allowed Hosts:
ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS', default=['example.com'])
Security:
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = env.bool('DJANGO_SECURE_SSL_REDIRECT', default=True)
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_CONTENT_TYPE_NOSNIFF = True
Database:
DATABASES['default'] = env.db('DATABASE_URL')
DATABASES['default']['ATOMIC_REQUESTS'] = True
DATABASES['default']['CONN_MAX_AGE'] = env.int('CONN_MAX_AGE', default=60)
Caching (Redis):
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': env('REDIS_URL'),
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'IGNORE_EXCEPTIONS': True,
}
}
}
Static Files (WhiteNoise):
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
Media Files (S3):
if env.bool('USE_S3', default=False):
AWS_ACCESS_KEY_ID = env('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = env('AWS_SECRET_ACCESS_KEY')
AWS_STORAGE_BUCKET_NAME = env('AWS_STORAGE_BUCKET_NAME')
AWS_S3_REGION_NAME = env('AWS_S3_REGION_NAME', default='us-east-1')
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
Email (Production service):
EMAIL_BACKEND = env('DJANGO_EMAIL_BACKEND', default='anymail.backends.mailgun.EmailBackend')
Logging:
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
},
},
'handlers': {
'console': {
'level': 'INFO',
'class': 'logging.StreamHandler',
'formatter': 'verbose'
},
},
'root': {
'level': 'INFO',
'handlers': ['console'],
},
}
Sentry:
if env('SENTRY_DSN', default=None):
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
sentry_sdk.init(
dsn=env('SENTRY_DSN'),
integrations=[DjangoIntegration()],
environment=env('SENTRY_ENVIRONMENT', default='production'),
)
Environment Selection#
Via Environment Variable:
export DJANGO_SETTINGS_MODULE=construbot.config.settings.production
python manage.py runserver
Via Makefile:
make dev # Uses local settings
make buildprod # Uses production settings
make test # Uses test settings
In manage.py:
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'construbot.config.settings.local')
django-environ#
Loading .env file:
# In base.py
import environ
ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent.parent
APPS_DIR = ROOT_DIR / 'construbot'
env = environ.Env()
# Read .env file
READ_DOT_ENV_FILE = env.bool('DJANGO_READ_DOT_ENV_FILE', default=False)
if READ_DOT_ENV_FILE:
env.read_env(str(ROOT_DIR / '.env'))
Accessing variables:
DEBUG = env.bool('DJANGO_DEBUG', default=False)
SECRET_KEY = env('DJANGO_SECRET_KEY')
DATABASE_URL = env.db('DATABASE_URL')
ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS', default=[])
Library Mode#
CONSTRUBOT_AS_LIBRARY setting:
When True, disables standalone features:
# In base.py
CONSTRUBOT_AS_LIBRARY = env.bool('CONSTRUBOT_AS_LIBRARY', default=False)
if not CONSTRUBOT_AS_LIBRARY:
INSTALLED_APPS += [
'construbot.account_config', # Only in standalone mode
]
Affects:
Admin interface availability
Account management URLs
Authentication system
See Library Mode for details.
Best Practices#
1. Never commit secrets:
# Use environment variables
SECRET_KEY = env('DJANGO_SECRET_KEY')
# Never hardcode:
# SECRET_KEY = 'abc123' # BAD!
2. Use sensible defaults:
DEBUG = env.bool('DJANGO_DEBUG', default=False) # Safe default
3. Document required variables:
Create .env.example with all required variables.
4. Validate production settings:
python manage.py check --deploy
5. Keep base.py DRY:
Put common settings in base.py, override only what differs.
Troubleshooting#
Wrong settings module loaded:
# Check current settings
python manage.py diffsettings
# Verify environment variable
echo $DJANGO_SETTINGS_MODULE
Settings not loading from .env:
# Ensure this is set
export DJANGO_READ_DOT_ENV_FILE=True
# Or in .env file itself (chicken-egg problem)
# Better: export it before running Django
Import errors:
# Always import from base
from .base import *
from .base import env # Don't forget this!
See Also#
Overview - Architecture overview
Configuration - Environment configuration
Environment Variables - Production variables