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 environments

  • local.py - Development settings

  • test.py - Testing settings

  • production.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#