# FastAPI environment variables with pydantic-settings

> FastAPI reads settings through pydantic-settings, where environment variables beat .env files. Types, caching, secret files and running FastAPI with Crunu.

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

FastAPI has no environment variable loader of its own. The [FastAPI documentation](https://fastapi.tiangolo.com/advanced/settings/) recommends pydantic-settings: you write a `Settings` class that inherits from `BaseSettings`, and creating it reads each field from the environment, matching names case-insensitively and converting strings to the declared types. A `.env` file is read only if you set `env_file` in the model config and install python-dotenv, and a real environment variable always beats the same name in that file. Settings are usually created once at startup, so they behave as runtime values that change on restart. Checked October 30, 2026, with pydantic-settings 2.16.

## What pydantic-settings reads, and in what order

A settings class like this one declares the shape:

```text
class Settings(BaseSettings):
    app_name: str = "Orders API"
    database_url: str
    items_per_user: int = 50

    model_config = SettingsConfigDict(env_file=".env")
```

When `Settings()` runs, each field is filled from the first source that has it. The [Pydantic settings documentation](https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/) gives the default order:

| Priority | Source                                   |
| -------- | ---------------------------------------- |
| 1        | arguments passed to `Settings(...)`      |
| 2        | environment variables                    |
| 3        | the dotenv file or files from `env_file` |
| 4        | files in `secrets_dir`                   |
| 5        | field defaults                           |

*Figure: Diagram of how FastAPI loads environment variables. Env files are read in this order of priority, and the first one wins: .env. 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 documentation states it plainly: even when using a dotenv file, environment variables will always take priority over values loaded from it. If `env_file` lists several files, they load in order and later files override earlier ones.

## Types, names and nesting

Environment variables are text. pydantic-settings converts them to the declared types and validates them, so `items_per_user` arrives as an `int`, and `ITEMS_PER_USER=lots` stops the app with a validation error instead of failing later. A field without a default is required. If no source provides it, startup fails, which is the behavior you want on a server.

Names are matched case-insensitively by default, so `DATABASE_URL` fills `database_url`. Two settings help larger projects:

- **`env_prefix`** adds a prefix to every name, for example `ORDERS_DATABASE_URL`. It applies to dotenv files and secret files too, not only to the environment. The default is empty.
- **`env_nested_delimiter`** maps names like `DB__HOST` onto nested models. Nested variables take precedence over a top-level JSON value for the same model.

Wrap secrets in [`SecretStr`](https://pydantic.dev/docs/validation/latest/api/pydantic/types/). The value is still available through `get_secret_value()`, but it prints as asterisks in logs and in `repr()`, which saves you from the most common accidental leak: logging the whole settings object at startup.

## Create settings once

The FastAPI docs show settings provided through a dependency decorated with `@lru_cache`:

```text
@lru_cache
def get_settings():
    return Settings()
```

Without the cache, every request that depends on settings would create a new object and read the `.env` file from disk again. With it, the object is created on first use and reused. The dependency form also makes testing easy: `app.dependency_overrides[get_settings]` can return a `Settings` built with test values, and nothing in the environment needs to change.

A consequence of caching: a running worker never sees a changed variable. Restarting the process is the way to apply new values.

## Secret files

`secrets_dir` reads one file per field from a directory. With `secrets_dir="/run/secrets"`, which is where Docker mounts secrets by default, a file named `database_url` fills `database_url`. A missing directory only produces a warning. Since files rank below environment variables and the dotenv file, a stray variable with the same name silently wins over the mounted secret, which is worth knowing when you debug.

## A short review of your Settings class

The settings class is the single place where your API meets its configuration, which makes it worth a review whenever someone adds a field. A few questions catch most problems before they reach production:

- **Does any secret have a default?** A default password or token in code ends up in Git and in every fork. Secrets should be required fields with no default, so a missing value stops the app.
- **Is every secret typed as `SecretStr`?** Plain `str` fields print their values the first time someone logs the settings object or an exception includes it.
- **Does anything else read `os.environ` directly?** Scattered lookups bypass validation and type conversion, and make it hard to answer which variables the service needs. Route them through the settings class.
- **Does `.env.example` list every field?** It's the document a new teammate reads first. Keep it in the repository with harmless placeholders, and keep the real `.env` out of it.
- **Is the same class used by background workers?** A Celery or arq worker that builds its own configuration can drift from the API's. Share the class, and give the worker its own environment if it needs fewer secrets.

None of this depends on where the values come from, but it makes moving them, to Crunu or anywhere else, a change of source rather than a rewrite.

## Run FastAPI with values from Crunu

`crunu run` decrypts a Crunu environment in memory and starts your command with the variables in its process environment. That is source number two in the table, above every file, so the Crunu values fill your `Settings` without python-dotenv and without a `.env` file on disk. The FastAPI CLI, Uvicorn and Gunicorn all inherit the variables from `crunu run`.

1. Store each stage's values in a Crunu environment, for example `api/development` and `api/production`, using the same names your `Settings` fields expect.
2. Develop with `crunu run --env api/development -- fastapi dev main.py` from your activated virtual environment.
3. Run tests with `crunu run --env api/test -- pytest`, or skip Crunu there and use `dependency_overrides` for fully local test values.
4. On a Linux server with the agent, start the app with `crunu run --env api/production --restart on-change -- fastapi run main.py`.

With `--restart on-change`, `crunu` sends `SIGTERM` after a value changes and starts the server again, so the cached `Settings` object is rebuilt with the new values. That also covers a rotated database password, as long as the old one stays valid until the restart completes.

Things to know about the clean environment that `crunu run` creates: it passes `PATH`, `HOME` and a few locale and terminal variables from your shell, plus the Crunu variables. `PATH` is enough for an activated virtual environment to be found. `PYTHONPATH` is not passed, so add it to the Crunu environment or use `--inherit-env` if your project depends on it. If your `Settings` uses an `env_prefix`, the names in Crunu need the prefix too.

## Where FastAPI apps usually run

In containers, start the server under `crunu run` with a service token, or mount secret files and use `secrets_dir`, but not both for the same values, since the environment wins over the files. Whoever stores a service token for a container can decrypt its environment, so give each token one environment and read the limits on the [Fly.io page](https://crunu.com/environment-variables/fly-io). The [Docker Compose page](https://crunu.com/environment-variables/docker-compose) shows both patterns. On hosting platforms like [Render](https://crunu.com/environment-variables/render), the platform's own variables reach pydantic-settings the same way; Crunu doesn't sync into them.

## Related reading

- [Django](https://crunu.com/environment-variables/django) and [Flask](https://crunu.com/environment-variables/flask) for the other Python frameworks.
- [How to share .env files with your team](https://crunu.com/guides/share-env-files-with-a-team).
- [Runtime secret injection](https://crunu.com/learn/runtime-secret-injection) and [what a .env file is](https://crunu.com/learn/dotenv-file).
- [crunu run](https://crunu.com/docs/cli/run) and [installing the agent](https://crunu.com/docs/getting-started/install-the-agent).

## FAQ

### If a variable is set in the environment and in .env, which one does FastAPI use?

The environment variable. pydantic-settings reads both, and environment variables always take priority over values loaded from a dotenv file.

### Can pydantic-settings read Docker secrets?

Yes. Set secrets_dir to /run/secrets in the settings model config and each file there becomes a value for the field of the same name. Environment variables and the .env file still take priority over those files.

### Why use a dependency with lru_cache for settings?

Creating the Settings object reads the environment and the .env file. Caching the dependency means that happens once, and tests can still override it through dependency_overrides.

### Do I need python-dotenv when I use crunu run?

No. crunu run puts the values into the process environment, which pydantic-settings reads without any extra package. Keep env_file only if you want a non-secret defaults file.

## Sources

- [FastAPI: Settings and environment variables](https://fastapi.tiangolo.com/advanced/settings/) (accessed 2026-10-30)
- [Pydantic: Settings management](https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/) (accessed 2026-10-30)
- [Pydantic: Types, SecretStr](https://pydantic.dev/docs/validation/latest/api/pydantic/types/) (accessed 2026-10-30)
