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.
Part of the guide
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
- Crunu command
crunu run --env django-app/development -- python manage.py runserverSee the systemd integration
Common pitfalls
- Django reads no .env file by itself. settings.py sees only os.environ unless you add a loader such as python-dotenv.
- Django refuses to start without SECRET_KEY, so a manage.py command run during an image build, such as collectstatic, needs some value.
- Changing SECRET_KEY invalidates sessions and password reset links unless the old key moves to SECRET_KEY_FALLBACKS first.
- Environment values are strings: a DEBUG setting read straight from os.environ is truthy even when it holds the word False.
- 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:
- Django reads
DJANGO_SETTINGS_MODULE, for examplemysite.settings, or uses--settingson the command line. Without either, and withoutsettings.configure(), the first access to a setting raisesImportError. - Python imports that module. Every line in it runs, including any
os.environlookups.
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.pythat 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 useos.environ["NAME"]rather than.get(). - A settings package with
base,developmentandproductionmodules, chosen byDJANGO_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:
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.
- Put
DJANGO_SECRET_KEY,DATABASE_URL,DEBUGandDJANGO_SETTINGS_MODULEinto Crunu environments such asdjango-app/developmentanddjango-app/production, with a different secret key in each. - Develop with
crunu run --env django-app/development -- python manage.py runserverfrom your activated virtual environment. - Run one-off commands the same way, for example
crunu run --env django-app/production -- python manage.py migrateon a machine that is allowed to reach production. - 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 runkeepsPATHfrom your shell, so an activated virtual environment'spythonis found. It doesn't passPYTHONPATHorVIRTUAL_ENVby default. Add what your project needs to the environment, or use--inherit-env. - Restarts are graceful. With
--restart on-change,crunusendsSIGTERMto 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.
Related reading
- Flask and FastAPI, the other Python frameworks on this site.
- Rails solves the same problem with encrypted credentials.
- How to share .env files with your team.
- Key rotation and crunu run.
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.
- Django 6.1: Deployment checklist · checked October 30, 2026
- Django 6.1: Settings · checked October 30, 2026
- Django 6.1: Django settings (topic guide) · checked October 30, 2026
- python-dotenv README · checked October 30, 2026