Environment Variables

As we’re following the 12 factors, everything can be configured using environment variables.

Environment variables

  • Required basic values — Those things cannot be invented nor have decent default value. You need to specify them.

    • DATABASE_URL — The URL to the database (must be a PostgreSQL DB)

    • ENVIRONMENT — Name of the environment (current branch name, develop, etc). That’s only required in production, if the current user seems to have a home in /home we’ll invent an environment name based on the user name and the DB name.

    • SECRET_KEY — Django’s secret key value

  • Cache/Queue

    • REDIS_URL — Base URL of the Redis. It will be used by several services (cache, queue, etc), each with a different (auto-generated) prefix.

  • Helper features

    • DEBUG (YAML) — Enables debug mode

    • POOL_DB_CONNECTIONS (YAML) — Enable this in production to enable DB connection pooling

    • SENTRY_DSN — Set here the Sentry DSN to enable reporting exceptions to Sentry

    • TIME_ZONE — Default time zone to be used by Django

  • External communications — See below

External communications

The preset comes with Wailer and thus the ability to send emails or SMS.

Both systems work with the same idea:

  • Can be entirely configured with environment variables

  • Supports different backends

  • Safe by default: print to console unless asked otherwise and pure in-memory implementation during unit testing, so that communications don’t leave the developer’s machine

Email

The emails can be configured with the EMAIL_MODE environment variables, that has 3 possible values, with associated environment variables:

  • console — Default print-to-console backend

  • mailjet — Using Mailjet to send emails

    • MAILJET_API_KEY_PUBLIC — Public API key

    • MAILJET_API_KEY_PRIVATE — Private API key

  • mandrill — Using Mandrill to send emails

    • MANDRILL_API_KEY — Mandrill’s API key

SMS

It’s the same concept, driven by SMS_MODE:

  • console — Default print-to-console backend

  • mailjet — Using Mailjet to send SMSes

    • MAILJET_API_TOKEN — API token for SMS sending

Preset implementation

Here’s the documentation from the preset code to understand how those are used:

class model_w.preset.django.ModelWDjango(*, sentry_sample_rate: float = 1.0, base_dir: str | Path | None = None, enable_postgis: bool = False, default_time_zone: str = 'Europe/Madrid', url_prefix: str = '/back', conn_max_age_when_pooled: int | float = 60, enable_cache: bool = True, enable_celery: bool | None = None, celery_task_track_started: bool = True, celery_task_time_limit: float | int = 3600, enable_channels: bool | None = None, enable_wagtail: bool | None = None, enable_storages: bool | None = None, enable_health_check: bool | None = None)

ModelW preset for Django running on PaaS platforms like DigitalOcean. Feel free to override any method if it doesn’t suit your needs. All values that are set in pre_ methods can easily be overridden by just setting it from inside the settings.py.

__init__(*, sentry_sample_rate: float = 1.0, base_dir: str | Path | None = None, enable_postgis: bool = False, default_time_zone: str = 'Europe/Madrid', url_prefix: str = '/back', conn_max_age_when_pooled: int | float = 60, enable_cache: bool = True, enable_celery: bool | None = None, celery_task_track_started: bool = True, celery_task_time_limit: float | int = 3600, enable_channels: bool | None = None, enable_wagtail: bool | None = None, enable_storages: bool | None = None, enable_health_check: bool | None = None)

You can set here different adjustments within the supported options

Parameters:
  • sentry_sample_rate – Trace rate for Sentry performance tracing. Defaults to 1 for new projects but at scale you must reduce this value.

  • base_dir – Base dir of the Django project. In most setups that exist, this is the directory that contains the manage.py file. If this parameter is missing, that’s how we’ll determine the base directory, based on the location of manage.py.

  • enable_postgis – If your project does some geographical stuff, you need to enable PostGIS using this flag.

  • default_time_zone – The time zone to set by default in Django. Not very neutral default value for sure but we’re a Madrid-based company so…

  • url_prefix – In PaaS platforms, it is convenient to have all the calls to the backend prefixed with a folder name. Here it’s /back by default, meaning that you should configure your URLs to be /back/admin, /back/api and so forth. The usefulness of this value is for the settings like the static URL prefix.

  • conn_max_age_when_pooled – There is a POOL_DB_CONNECTIONS environment variable that enables connection pooling (by default connections will open/close at each request, which is slower but more cost-effective in some cases). This value indicates the conn_max_age (in seconds) when pooling is enabled.

  • enable_cache – Enables the cache engine (in Redis).

  • enable_celery – Enables a default configuration for Celery, that can then be overridden. By default it will enable itself if Celery can be imported. Celery will be installed if you install the celery extra dependency (pip install modelw-preset-django[celery]).

  • celery_task_track_started – Enable or not the tracking of tasks start

  • celery_task_time_limit – Maximum duration before a worker gets killed on a task (hard limit). Choose something wide but choose something. The default of 1h seems reasonable. This avoids stalled processes (requests without timeout…) clogging the queue.

  • enable_channels – Enables Django Channels (which will configure it to use Redis). By default it will detect if channels is present and enable it automatically if so. You can just request the “channels” extra to this package.

  • enable_health_check – Enables the health check system. It will be automatically enabled if django-health-check is installed. It will check if celery is also installed and add the appropriate apps related to it.

pre_base_dir()

Saving the base dir into settings

pre_sentry(env: EnvManager)

Configures Sentry if the DSN is found

pre_basic_stuff(env: EnvManager)

A bunch of quite basic things that are common to all projects

pre_logging(env)

Configuring nice console logging, especially when in debug mode. We’re setting exceptions for a lot of commonly used root modules that would otherwise spam the console.

pre_database(env: EnvManager)

Database has to be PostgreSQL. We’re using dj_database_url to parse the settings, but with a bunch of overridden things in the middle (like the connection pooling configuration or using the psqlextra backend).

pre_password()

Our default password hasher is Argon2. It’s much more secure than the rest for a much lower cost. The only reason why it’s not the default in Django is that it requires an external dependency (but we don’t mind depending on it so all is fine).

pre_languages(env: EnvManager)

Enabling localization features

post_languages(context)

We’re checking that some LANGUAGES are set and we can use the first language as default language for the app (this way you don’t need to set LANGUAGES and LANGUAGE_CODE separately).

pre_static_files()

Configuring here static files. They’ll be served by WhiteNoise (see below).

pre_middleware()

We’re putting here the middlewares that come with a default Django project (as opposed to the default in Django which is just empty). Overall, good luck running your website without those.

It is recommended that from your settings.py file you don’t replace this list but rather you add your own middlewares to it by doing something like:

>>> MIDDLEWARE = []
>>> with EnvManager() as env:
>>>     MIDDLEWARE.append("my.middleware.Class")

Let’s note in the example above the MIDDLEWARE = [] line. It’s just so that the IDE is not confused when EnvManager() sneakily adds MIDDLEWARE to local variables. It will still contain the values defined below, not an empty array.

The reason to do like this is that if one day Django or Model W come with more interesting middlewares it’d be a shame to miss on them because you overrided the whole thing.

post_whitenoise(context)

We use WhiteNoise as static files storage engine and we make sure that it is present within middlewares.

pre_templates()

Basic template config from default Django settings

pre_cache(env)

Let’s configure the cache engine to use Redis

pre_wailer()

We’re just defining those so that Wailer doesn’t crash, however we’ll let the user define their own emails/sms when then want.

post_wailer(context)

Making sure that Wailer is installed in the apps

post_email(env: EnvManager)

Configuring emails sending. Unless an environment variable explicitly enables emails, they will be printed on the console for obvious safety reasons.

post_sms(env: EnvManager)

Configuring SMS sending. Unless an environment variable explicitly enables SMSes, they will be printed on the console for obvious safety reasons.

pre_drf()

Some basic DRF configuration, feel free to make it your own

post_drf(context)

We’re installing DRF

post_helper(context)

This preset comes with a Django app that brings a settings helper. It has no models on its own so it’s really just the command. We discretely insert the app into the INSTALLED_APPS, shouldn’t be too intrusive.

pre_celery(env: EnvManager)

When Celery is enabled, we inject some decent default using Redis as a broker and Django-backed backends.

post_celery(env: EnvManager, context)

These settings are put in post because we want to give a chance to the user to override the broker URL. If they didn’t do it, we proceed to hijacking the broker options to enforce a prefix in Redis keys.

pre_channels(env: EnvManager)

If Channels is enabled we need to configure the broker to be Redis and to be prefixing properly its keys (otherwise we’re at risk of conflict with the other parts of the app that also use Redis).

post_channels(context)

Trying to be nice and adding channels to the installed apps.

pre_storages(env)

If storages is enabled, we’ll look to enable S3.

There can be two modes to work with S3:

  • s3, when it’s S3 storage on AWS cloud

  • do, when it’s S3 storage on DigitalOcean

Depending on this, different environment variables will be required. It will also detect if we’re running from inside an AWS container or something of the sort in order to avoid manually setting the access key when Boto3 will determine it on its own.

In do mode, the lib is configured to use the DigitalOcean endpoint instead of the default AWS one.

pre_wagtail()

Reasonable default settings for Wagtail

post_wagtail(context)

Making sure Wagtail components and middlewares are loaded

pre_base_url(env)

Defining a BASE_URL and WAGTAIL_BASE_URL environment variable which is used both by Wagtail and by Wailer and potentially other systems in order to know which is the BASE_URL we should use for this website. It’s helpful because things don’t always come from an HTTP query (like crons) and thus you cannot always know the Host header.

pre_health_check(env: EnvManager)

If health check is enabled, we’ll look to enable the health check system.

post_health_check(context)

Making sure that Health Check is installed in the apps