Skip to content
FrameworkPython

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.

DjangoAt a glance

Env files, highest priority first
No .env files: values come from the process environment or the platform.
Public prefix
None

No names are copied into browser code by a prefix.

Values applied
At runtime

Values are read from the environment when the app runs.

Built-in secret store
None: settings.py reads os.environ

docs.djangoproject.com/en/6.1/howto/deployment/checklist

Crunu command
crunu run --env django-app/development -- python manage.py runserverSee the systemd integration

Common pitfalls

  1. Django reads no .env file by itself. settings.py sees only os.environ unless you add a loader such as python-dotenv.
  2. Django refuses to start without SECRET_KEY, so a manage.py command run during an image build, such as collectstatic, needs some value.
  3. Changing SECRET_KEY invalidates sessions and password reset links unless the old key moves to SECRET_KEY_FALLBACKS first.
  4. Environment values are strings: a DEBUG setting read straight from os.environ is truthy even when it holds the word False.
  5. PYTHONPATH from your shell doesn't reach Django under crunu run. Put it in the environment or pass --inherit-env.
On this page

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 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 states (override=False). So a real environment variable beats the file.

The settings topic guide 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, 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 and the systemd integration.

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 goes through it, and the Django SECRET_KEY generator creates a new one in your browser.

Frequently asked questions

Something else? Write to support@crunu.com

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

Every source this page relies on, with the date we last checked it.

  1. Django 6.1: Deployment checklist · checked October 30, 2026
  2. Django 6.1: Settings · checked October 30, 2026
  3. Django 6.1: Django settings (topic guide) · checked October 30, 2026
  4. python-dotenv README · checked October 30, 2026

Keep your .env values in one place

Free during early access. Values are encrypted on your devices, each environment has its own key, and crunu run hands them to your app without writing a file.