Flask environment variables: .flaskenv and .env
The flask command loads .env and .flaskenv, but production servers don't. How Flask config reads the environment, FLASK_ prefixes and running Flask with Crunu.
Part of the guide
FlaskAt a glance
- Env file that wins
.envFirst of 2 .env files, highest priority first. See the load order
- 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: app.config is filled from code, files and the environment
- Crunu command
crunu run --env flask-app/development -- flask --app hello run --debugRecipeCrunu has no dedicated integration here: this is the general setup.
Common pitfalls
- .env and .flaskenv are loaded only by the flask command or app.run(). Gunicorn and other production servers never read them.
- Without python-dotenv installed, Flask only prints a tip and loads neither file.
- The flask command searches upward from the current directory, so a .env in a parent folder can end up in your app.
- from_prefixed_env parses values as JSON: only lowercase true and false become booleans, and any other text stays a truthy string.
- Setting DEBUG in code is unreliable. Use the --debug option or FLASK_DEBUG instead.
On this page
Flask reads its configuration into app.config, a dictionary you fill from code, from files or from environment variables. The flask command adds a convenience on top: when python-dotenv is installed, it loads .flaskenv and .env into the environment before your app starts, and a variable already set in the shell wins over both files. That loading happens only through the flask command or app.run(). A production server such as Gunicorn reads neither file. These rules come from the Flask 3.1 CLI documentation, checked October 30, 2026.
The two files the flask command loads
| Priority | Source | Meant for |
|---|---|---|
| 1 | variables set in the shell | anything |
| 2 | .env | private values, not committed |
| 3 | .flaskenv | public settings like FLASK_APP, committed |
The --env-file option names one more file to load for a run. The documentation doesn't rank it against the other two, so don't rely on it to override them.
Flask looks for these files by scanning upward from the directory where you call flask. That's handy in a monorepo and surprising when an old .env in a parent directory quietly supplies a value.
If python-dotenv isn't installed, Flask doesn't load anything. It prints a tip that .env files are present and suggests installing it. Setting FLASK_SKIP_DOTENV=1 turns loading off even when the package is there, which is useful when another tool already loads the files. The variables must be set before the app is created, or it won't be configured as expected.
The command's own options also come from the environment. FLASK_APP replaces --app, and every option of every command maps to FLASK_COMMAND_OPTION, such as FLASK_RUN_PORT=8000 for flask run --port 8000. Those belong in .flaskenv.
Getting values into app.config
The environment and app.config are separate. A variable in the environment doesn't become configuration until your code copies it. The configuration guide offers several ways:
app.config.from_prefixed_env()copies every variable that starts withFLASK_into the config without the prefix.FLASK_SECRET_KEYbecomesSECRET_KEY. A different prefix can be passed as an argument.app.config.from_envvar("APP_SETTINGS")reads a Python config file whose path is stored in that variable.os.environin your own config module or factory, for full control.
from_prefixed_env() parses each value with json.loads, so numbers, lists and dictionaries work. For booleans, only lowercase true and false are recognized. False with a capital letter isn't valid JSON, stays a string, and any non-empty string is true in Python. A double underscore builds nested keys, so FLASK_MYAPI__credentials__username lands in app.config["MYAPI"]["credentials"]["username"].
Load configuration early, before extensions call init_app, and avoid code that needs configuration at import time. Both recommendations come straight from the guide, and both make it easier to start the same app with different values in tests.
SECRET_KEY and debug mode
SECRET_KEY signs the session cookie and is available to extensions for their own security needs. The guide suggests generating it with Python's secrets module and warns not to reveal it when posting questions or committing code. Flask 3.1 added SECRET_KEY_FALLBACKS, a list of old keys still accepted for unsigning, so you can rotate the key without invalidating active sessions. Flask's built-in session supports it. Extensions that use the key may not yet.
Debug mode is special. Flask strongly discourages setting DEBUG in code or config, because the flask run command can't read it early and extensions may already have configured themselves. Use flask run --debug or FLASK_DEBUG, and never enable it in production. The documentation puts that sentence in bold for a reason: the interactive debugger is built for your own machine, not for visitors.
Tests and the app factory
The configuration guide recommends creating the app in a function and registering blueprints inside it. That pattern pays off for environment handling. The factory can read the environment for normal runs, and a test can call the same factory with explicit test values instead, so tests never depend on whatever happens to be exported in a developer's shell or written in their .env.
A practical split looks like this. A committed default configuration holds safe values for everything. The factory then overrides them from the environment with from_prefixed_env(). Tests pass a mapping with TESTING set and a throwaway SECRET_KEY, and skip the environment entirely. Each layer is visible in code, and a reviewer can tell which values a deployment must supply by reading the defaults: anything that is empty there has to come from the environment.
Production servers don't read .env
This is the Flask surprise that reaches production. The app works with flask run on a laptop because the command loaded .env. Deployed under Gunicorn, uWSGI or another WSGI server, nothing loads that file, so SECRET_KEY or the database URL is missing. Flask's own advice is to call load_dotenv() manually if you want the files in production. The alternative is to set real environment variables, which is what the next section does.
Run Flask with values from Crunu
crunu run starts your program with the variables of a Crunu environment in its process environment. For Flask, those count as shell variables, the top row of the table, so they win over .env and .flaskenv. The same command works for the development server and for Gunicorn, so the "works locally, fails in production" gap disappears.
- Keep
FLASK_APPand other public options in a committed.flaskenv, and putFLASK_SECRET_KEY, database credentials and API keys into Crunu environments such asflask-app/developmentandflask-app/production. - Develop with
crunu run --env flask-app/development -- flask --app hello run --debug. - Call
app.config.from_prefixed_env()in your app factory soFLASK_variables reachapp.config. - On a Linux server with the agent, start Gunicorn with
crunu run --env flask-app/production --restart on-change -- gunicorn 'hello:create_app()'.
With --restart on-change, a changed value makes crunu stop Gunicorn with SIGTERM and start it again, so the app factory reads the new configuration. crunu run passes PATH and a few other basics from your shell, which is enough for an activated virtual environment, but not PYTHONPATH. Add it to the environment or use --inherit-env if your layout depends on it.
You can delete .env once the values live in Crunu. Leaving it around is harmless for priority, since the Crunu values win, but it keeps a plaintext copy of old secrets on disk and in backups.
Related reading
- Django, which has no dotenv loading at all.
- FastAPI with pydantic-settings.
- Render, a common host for Flask apps.
- How to share .env files with your team and loading an env file into a shell safely.
Frequently asked questions
Something else? Write to support@crunu.com
What is the difference between .flaskenv and .env?
Both are loaded by the flask command. .flaskenv is meant for public settings such as FLASK_APP and can be committed. .env is for private values and should stay out of the repository. Values in .env win over .flaskenv.
Why does my app work with flask run but not under Gunicorn?
The flask command loads .env and .flaskenv. Gunicorn doesn't. In production, set real environment variables, call load_dotenv yourself, or start Gunicorn under crunu run.
How do I read all FLASK_ variables into app.config?
Call app.config.from_prefixed_env(). FLASK_SECRET_KEY becomes SECRET_KEY, values are parsed as JSON, and a double underscore creates nested keys.
How do I rotate the Flask SECRET_KEY without logging users out?
Since Flask 3.1, put the old key in SECRET_KEY_FALLBACKS and the new one in SECRET_KEY. Flask's session cookie accepts both. Extensions that use the key may not support fallbacks yet.
Sources
Every source this page relies on, with the date we last checked it.
- Flask: Command line interface · checked October 30, 2026
- Flask: Configuration handling · checked October 30, 2026