# Django environment variables and SECRET_KEY

> Django reads settings from Python code, not .env files. How to load SECRET_KEY and other secrets from the environment, rotate them, and run Django with Crunu.

URL: https://crunu.com/environment-variables/django
Language: en
Updated: 2026-10-30

Django doesn't read environment variables or `.env` files on its own. Its configuration is a Python module, named by the `DJANGO_SETTINGS_MODULE` variable, and that module reads whatever it wants from `os.environ` when the process starts. The [Django 6.1 deployment checklist](https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/) recommends exactly that for the most sensitive value, writing `SECRET_KEY = os.environ["SECRET_KEY"]` instead of a hard-coded key. Values are read once at startup, so a change needs a restart. This page was checked against the Django 6.1 documentation on October 30, 2026.

## How Django finds its settings

Two things happen before your code runs:

1. Django reads `DJANGO_SETTINGS_MODULE`, for example `mysite.settings`, or uses `--settings` on the command line. Without either, and without `settings.configure()`, the first access to a setting raises `ImportError`.
2. Python imports that module. Every line in it runs, including any `os.environ` lookups.

There is no file list and no precedence chain. If you want a `.env` file, a library has to load it at the top of `settings.py`. The common choice, python-dotenv, does not override variables that already exist in the environment by default, as its [README](https://github.com/theskumar/python-dotenv) states (`override=False`). So a real environment variable beats the file.

*Figure: Diagram of how Django loads environment variables. No env files are read by default; values come from the environment of the process. Values are read from the environment when the app starts. There is no public prefix: nothing reaches the browser unless your code sends it.*

The [settings topic guide](https://docs.djangoproject.com/en/6.1/topics/settings/) adds one rule worth repeating: don't change settings at runtime. Assigning `settings.DEBUG = True` in a view isn't supported. The settings file is the only place to set them.

## One settings module or several

Teams usually pick one of two layouts, and environment variables fit both:

- **One `settings.py` that reads everything from the environment.** Development and production differ only in their variables. This keeps one code path, and a missing variable fails loudly at startup if you use `os.environ["NAME"]` rather than `.get()`.
- **A settings package with `base`, `development` and `production` modules**, chosen by `DJANGO_SETTINGS_MODULE`. Non-secret differences, such as installed debug tools, live in code. Secrets are still read from the environment in each module.

Either way, decide which variables are required and read those with square brackets, so a misconfigured server refuses to start instead of running with an empty password or a default key. Optional values can use `.get()` with a safe default.

## SECRET_KEY: what it signs and how to rotate it

According to the [settings reference](https://docs.djangoproject.com/en/6.1/ref/settings/), `SECRET_KEY` is used for:

- sessions, unless you use the cache session backend and a custom session auth hash;
- messages stored in cookies;
- password reset tokens;
- any cryptographic signing that doesn't get its own key.

Django refuses to start when it isn't set. The reference also warns that running with a known key defeats many of Django's protections and can lead to privilege escalation and remote code execution. That's why the key generated by `startproject` and written into `settings.py` should be replaced before the first deploy and never committed again.

Rotation has built-in support. Put the new key in `SECRET_KEY` and the old one at the beginning of `SECRET_KEY_FALLBACKS`. Django signs with the new key and still accepts signatures made with the fallbacks. When you're ready to expire old sessions and reset links, remove the old key. Without the fallback step, every logged-in user is signed out at once and every outstanding reset link stops working. User passwords are not affected either way, because password hashes don't use the key.

In environment variables, that looks like two names:

```text
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
SECRET_KEY_FALLBACKS = [os.environ["DJANGO_OLD_SECRET_KEY"]]
```

Keep the fallback list short. Each extra key adds work to every signature check that doesn't match an earlier key.

## Strings, booleans and DEBUG

`os.environ` holds strings. `DEBUG = os.environ.get("DEBUG", False)` looks reasonable and is a classic mistake: the string `"False"` is truthy in Python, so production runs with debug pages on. The checklist is clear that debug must never be enabled in production, because it leaks source excerpts, local variables and settings. Parse booleans explicitly, for example by comparing with `"1"` or `"true"`, and default to off.

The global default of `DEBUG` is `False`, but the `settings.py` that `startproject` generates sets it to `True`. Read it from the environment before the first deploy.

## Images, collectstatic and other build steps

Because Django won't start without `SECRET_KEY`, any management command that imports your settings needs one, including `collectstatic` in a Dockerfile. People often bake a real key into the image to make the build pass. Use a throwaway value for build steps instead, and give the container the real key only when it starts. Never let the build and the running server share a key.

The same applies to database credentials. `collectstatic` doesn't need the database, so keep the build environment free of real connection strings.

## Run Django with values from Crunu

`crunu run` starts Django with the variables of a Crunu environment in its process environment. To `settings.py` they look like any other variable, so `os.environ["DJANGO_SECRET_KEY"]` simply works, with no `.env` file and no loader. If the project already uses python-dotenv, the Crunu values win, because the loader doesn't override existing variables.

1. Put `DJANGO_SECRET_KEY`, `DATABASE_URL`, `DEBUG` and `DJANGO_SETTINGS_MODULE` into Crunu environments such as `django-app/development` and `django-app/production`, with a different secret key in each.
2. Develop with `crunu run --env django-app/development -- python manage.py runserver` from your activated virtual environment.
3. Run one-off commands the same way, for example `crunu run --env django-app/production -- python manage.py migrate` on a machine that is allowed to reach production.
4. On a Linux server with the agent, start Gunicorn with `crunu run --env django-app/production --restart on-change -- gunicorn mysite.wsgi`.

A few details for Python projects:

- **The virtual environment still works.** `crunu run` keeps `PATH` from your shell, so an activated virtual environment's `python` is found. It doesn't pass `PYTHONPATH` or `VIRTUAL_ENV` by default. Add what your project needs to the environment, or use `--inherit-env`.
- **Restarts are graceful.** With `--restart on-change`, `crunu` sends `SIGTERM` to Gunicorn after a value changes and starts it again with the new values, which is how a rotated database password or a new fallback key reaches the running app.
- **Production stays out of CI.** The agent on the server fetches `django-app/production`, and the deploy pipeline only restarts the service. See [installing the agent](https://crunu.com/docs/getting-started/install-the-agent) and the [systemd integration](https://crunu.com/integrations/systemd).

## If the key leaked

If `SECRET_KEY` was committed, posted in chat or shipped in an image, treat it as exposed: generate a new key, move the old one to `SECRET_KEY_FALLBACKS` only if you need a short transition, and remove it soon after. While a leaked key is listed there, anyone who holds it can still create signatures that Django accepts. [What to do when a Django secret key leaks](https://crunu.com/blog/leaked-django-secret-key) goes through it, and the [Django SECRET_KEY generator](https://crunu.com/tools/secret-generator/django-secret-key) creates a new one in your browser.

## Related reading

- [Flask](https://crunu.com/environment-variables/flask) and [FastAPI](https://crunu.com/environment-variables/fastapi), the other Python frameworks on this site.
- [Rails](https://crunu.com/environment-variables/rails) solves the same problem with encrypted credentials.
- [How to share .env files with your team](https://crunu.com/guides/share-env-files-with-a-team).
- [Key rotation](https://crunu.com/learn/key-rotation) and [crunu run](https://crunu.com/docs/cli/run).

## FAQ

### Does Django support .env files?

Not by itself. A settings module is plain Python, so it can read os.environ directly. Projects that want a .env file add a loader such as python-dotenv or django-environ and call it at the top of settings.py.

### How do I rotate a Django SECRET_KEY without logging everyone out?

Set the new key as SECRET_KEY and put the previous one at the start of SECRET_KEY_FALLBACKS. Sessions and reset tokens signed with the old key keep working. Remove the old key once you are ready to expire them.

### Should DJANGO_SETTINGS_MODULE live in Crunu?

It can. It isn't secret, but keeping it in each environment means development and production can't be started with the wrong settings module by accident.

### Does SECRET_KEY protect user passwords?

No. Django doesn't use SECRET_KEY for password hashes, so rotating it doesn't affect them. It signs sessions, cookie-based messages, password reset tokens and other signed data.

## Sources

- [Django 6.1: Deployment checklist](https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/) (accessed 2026-10-30)
- [Django 6.1: Settings](https://docs.djangoproject.com/en/6.1/ref/settings/) (accessed 2026-10-30)
- [Django 6.1: Django settings (topic guide)](https://docs.djangoproject.com/en/6.1/topics/settings/) (accessed 2026-10-30)
- [python-dotenv README](https://github.com/theskumar/python-dotenv) (accessed 2026-10-30)
