# 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.

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

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](https://flask.palletsprojects.com/en/stable/cli/), 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 |

*Figure: Diagram of how Flask loads environment variables. Env files are read in this order of priority, and the first one wins: .env and .flaskenv. 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 `--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](https://flask.palletsprojects.com/en/stable/config/) offers several ways:

- **`app.config.from_prefixed_env()`** copies every variable that starts with `FLASK_` into the config without the prefix. `FLASK_SECRET_KEY` becomes `SECRET_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.environ` in 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.

1. Keep `FLASK_APP` and other public options in a committed `.flaskenv`, and put `FLASK_SECRET_KEY`, database credentials and API keys into Crunu environments such as `flask-app/development` and `flask-app/production`.
2. Develop with `crunu run --env flask-app/development -- flask --app hello run --debug`.
3. Call `app.config.from_prefixed_env()` in your app factory so `FLASK_` variables reach `app.config`.
4. 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](https://crunu.com/environment-variables/django), which has no dotenv loading at all.
- [FastAPI](https://crunu.com/environment-variables/fastapi) with pydantic-settings.
- [Render](https://crunu.com/environment-variables/render), a common host for Flask apps.
- [How to share .env files with your team](https://crunu.com/guides/share-env-files-with-a-team) and [loading an env file into a shell safely](https://crunu.com/blog/source-env-file-safely).

## FAQ

### 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

- [Flask: Command line interface](https://flask.palletsprojects.com/en/stable/cli/) (accessed 2026-10-30)
- [Flask: Configuration handling](https://flask.palletsprojects.com/en/stable/config/) (accessed 2026-10-30)
