# Environment Variables

There are three different things people mean by “environment variables” in a DDEV project, and they land in different places. This page covers all of them, in what order they override each other, and where to put a secret.

The same two scopes as [config options](<https://ddev.github.io/ddev/en/stable/users/configuration/config/index.md>) apply here:

1. **Project** files, in the project directory and mostly under `.ddev`.
2. **Global** files in the [global DDEV directory](<https://ddev.github.io/ddev/en/stable/users/usage/architecture/#global-files>), which apply to every project. This page writes that directory as `$HOME/.ddev`, its usual location, but it can be somewhere else.

> [!TIP]
>
> **Container variables take effect on restart**
>
> The `.ddev/.env*` files, their global equivalents, and `web_environment` are read while containers are being created, so run [`ddev restart`](<https://ddev.github.io/ddev/en/stable/users/usage/commands/#restart>) after changing them. An application `.env` file in the project root is not one of these: your framework reads it on every request, so changing it needs no restart.

## Which Variables Reach Where

| Where you set it | Type | What it reaches | Who reads it |
| --- | --- | --- | --- |
| An application `.env` file in the project root | project | Nothing in the container environment; it stays a file in your project | Your framework: Laravel, Craft CMS, Symfony, Silverstripe, and others |
| [`.ddev/.env*`](<#project-env-files>) and [`$HOME/.ddev/.env*`](<#global-env-files>) | project<br>global | The environment of any container in the project: `web`, `db`, and add-on or custom services | Anything running in the container, plus interpolation in `.ddev/docker-compose.*.yaml` |
| [`web_environment`](<#web_environment>) | project<br>global | The environment of the `web` container only | Anything running in `web` |

## Application `.env` Files, Outside `.ddev/`

Many frameworks read a `.env` file in the project root. That file belongs to your application, and DDEV treats it as ordinary project code: nothing in it becomes a container environment variable.

For several project types DDEV writes database credentials and the project URL into that file as part of [settings management](<https://ddev.github.io/ddev/en/stable/users/usage/cms-settings/index.md>), creating it if it doesn’t exist. Set [`disable_settings_management`](<https://ddev.github.io/ddev/en/stable/users/configuration/config/#disable_settings_management>) to `true` if you want to manage the file yourself.

You can edit it from the command line with [`ddev dotenv set`](<https://ddev.github.io/ddev/en/stable/users/usage/commands/#dotenv-set>), which is useful in a `README` or a CI script because it creates the file and updates single keys without disturbing the rest:

```
ddev dotenv set .env --app-key=value
```

Your framework reads this file on every request, so a change to it takes effect without a `ddev restart`.

The `.local` convention below applies only inside `.ddev/` and `$HOME/.ddev/`. A root `.env.local`, like the one Symfony and Shopware use, is your project’s file and is covered by your project’s own `.gitignore`.

## Project Env Files

The `.env*` files in the project’s `.ddev` directory set real environment variables in the project’s containers. The filename decides which service gets them:

```
.env[.<service>[.<label>...]][.local]
```

| File | Reaches | Notes |
| --- | --- | --- |
| `.ddev/.env` | Every container in the project |  |
| `.ddev/.env.local` | Every container in the project | Gitignored, DDEV v1.25.4+ |
| `.ddev/.env.web` | The `web` container | Any service name works: `db`, `redis`, and so on |
| `.ddev/.env.web.local` | The `web` container | Gitignored, DDEV v1.25.4+ |
| `.ddev/.env.web.myaddon` | The `web` container | A label keeps files from different sources apart, DDEV v1.25.4+ |
| `.ddev/.env.web.myaddon.local` | The `web` container | Gitignored, DDEV v1.25.4+ |
| `.ddev/.env.redis-build` | Nothing | No service is named `redis-build`, so this file is expanded but never injected |

A trailing `.local` only marks the file as gitignored, and a label after the service name only keeps files apart. Both are dropped before DDEV reads the service name, so `local` cannot be used as a service or label name. Files ending in `.example` are skipped, which is how an add-on ships a documented template without setting anything.

Every one of these files is also passed to `docker-compose config`, whatever it is named, so `${SOME_VARIABLE}` in a `.ddev/docker-compose.*.yaml` file is expanded from all of them together. That is the point of the `.ddev/.env.redis-build` row above: when a variable is only meant to be substituted into a compose file, give the file a name that is not a service name and it will never appear inside a container.

## Override Order

Later wins. Everything in the [global DDEV directory](<https://ddev.github.io/ddev/en/stable/users/usage/architecture/#global-files>) is applied before anything belonging to the project, and within each directory DDEV sorts the files by their name with a trailing `.local` removed, which puts a `.local` file directly after the file it overrides.

The table below is a project that has every kind of file at once, with `db` and `web` as its services and `myaddon` as a label. The global files, the `.local` files, and the labeled files all need DDEV v1.25.4+; `.ddev/.env` and `.ddev/.env.<service>` are older.

| \# | Content | Notes |
| --- | --- | --- |
| 1 | `web_environment` in `$HOME/.ddev/global_config.yaml` | Reaches the `web` container only |
| 2 | `web_environment` in `.ddev/config.yaml`, then `.ddev/config.*.yaml` | Reaches the `web` container only |
| 3 | `$HOME/.ddev/.env` | Every container of every project |
| 4 | `$HOME/.ddev/.env.local` | Overrides the file above it, gitignored |
| 5 | `$HOME/.ddev/.env.db` | Services in alphabetical order, so `db` before `web` |
| 6 | `$HOME/.ddev/.env.db.local` | Overrides the file above it, gitignored |
| 7 | `$HOME/.ddev/.env.web` | ‌ |
| 8 | `$HOME/.ddev/.env.web.local` | Overrides the file above it, gitignored |
| 9 | `$HOME/.ddev/.env.web.myaddon` | Labels in alphabetical order, all after the unlabeled `$HOME/.ddev/.env.web` |
| 10 | `$HOME/.ddev/.env.web.myaddon.local` | Overrides the file above it, gitignored |
| 11 | `.ddev/.env` | Every container of this project |
| 12 | `.ddev/.env.local` | Overrides the file above it, gitignored |
| 13 | `.ddev/.env.db` | Services in alphabetical order, so `db` before `web` |
| 14 | `.ddev/.env.db.local` | Overrides the file above it, gitignored |
| 15 | `.ddev/.env.web` | ‌ |
| 16 | `.ddev/.env.web.local` | Overrides the file above it, gitignored |
| 17 | `.ddev/.env.web.myaddon` | Labels in alphabetical order, all after the unlabeled `.ddev/.env.web` |
| 18 | `.ddev/.env.web.myaddon.local` | Overrides the file above it, gitignored, and applied last of all |

Only the files you actually have are applied, and the rest of the order is unchanged. A second label, `.ddev/.env.web.otheraddon`, would come after row 18, because `myaddon` sorts before `otheraddon` and a `.local` file never leaves the file it overrides.

A command run with `ddev exec`, `ddev ssh`, or a custom command also gets the [host variables forwarded on exec](<#host-variables-forwarded-on-exec>), which come after every row above and apply to that command only. A bare `web_environment` name is different: DDEV reads it from the host again on exec, but it keeps its place in row 1 or 2, so an env file that sets the same name still wins.

To see the result:

- [`ddev utility check-custom-config`](<https://ddev.github.io/ddev/en/stable/users/usage/commands/#utility-check-custom-config>) lists the env files DDEV found under `Environment`, in the order they are applied.
- [`ddev utility compose-config`](<https://ddev.github.io/ddev/en/stable/users/usage/commands/#utility-compose-config>) shows the rendered `.ddev/.ddev-docker-compose-full.yaml`, including the `environment` section of each service.
- `ddev exec env` and `ddev exec -s <service> env` show what a command run in a container gets, which includes the [host variables forwarded on exec](<#host-variables-forwarded-on-exec>).

## Global Env Files

With DDEV v1.25.4+, the same filenames work in the [global configuration directory](<https://ddev.github.io/ddev/en/stable/users/usage/architecture/#global-files>), where they apply to every project on the machine. `$HOME/.ddev/.env` reaches every container of every project, and `$HOME/.ddev/.env.db` reaches the `db` container of every project.

This is the only way to set a variable globally for something other than the `web` container, which is all [`web_environment`](<#web_environment>) can do.

Global files are applied before anything belonging to the project, so a global value is only a default: a project that sets the same variable in `.ddev/.env` or `.ddev/.env.<service>` wins. See [Override Order](<#override-order>) for the full sequence.

Use [`ddev dotenv global set`](<https://ddev.github.io/ddev/en/stable/users/usage/commands/#dotenv-global-set>) to write them. Name the file `.ddev/<file>` exactly as you would in a project, and DDEV resolves it in the global directory:

```
ddev dotenv global set .ddev/.env.web --api-url=https://example.com
```

## Secrets

With DDEV v1.25.4+, DDEV adds `/.env.local` and `/.env.*.local` to both `.ddev/.gitignore` and `$HOME/.ddev/.gitignore`, so any env file ending in `.local` stays out of Git without you having to take over those files.

That gives a shared service a natural split: commit `.ddev/.env.web` with the settings your team shares, and keep the token next to it in `.ddev/.env.web.local`, which is applied right after it.

```
# Committed, everyone gets it
ddev dotenv set .ddev/.env.web --api-url=https://example.com

# Gitignored, yours only
ddev dotenv set .ddev/.env.web.local --api-key=secret
```

To document which keys are expected without committing their values, add a `.ddev/.env.web.example` file listing the keys with empty values. DDEV never reads `.example` files, and `.ddev/.gitignore` ignores them, so commit it with `git add -f .ddev/.env.web.example`.

The global files work the same way, so `$HOME/.ddev/.env.web.local` is a reasonable home for a credential you use in every project (but note that it is overridden by project-level `.ddev/.env` or `.ddev/.env.web`, etc).

## `web_environment`

The [`web_environment`](<https://ddev.github.io/ddev/en/stable/users/configuration/config/#web_environment>) setting predates the env files above and only reaches the `web` container. It lives in `.ddev/config.yaml` or any `.ddev/config.*.yaml` for a project, and in `$HOME/.ddev/global_config.yaml` for every project:

```
web_environment:
    - MY_ENV_VAR=someval
    - MY_OTHER_ENV_VAR=someotherval
```

You can also pass a variable through from the host by listing just its name with no value. DDEV hands the bare name to docker-compose, which resolves it from the host environment when the container starts:

```
web_environment:
    - MY_HOST_VAR
```

DDEV also reads each bare name from the host again on every [`ddev exec`](<https://ddev.github.io/ddev/en/stable/users/usage/commands/#exec>), [`ddev ssh`](<https://ddev.github.io/ddev/en/stable/users/usage/commands/#ssh>), or custom command run in `web`, so a value set after `ddev start`, for example by an AI agent, reaches that command without a restart:

- Only that command sees the new value. PHP-FPM and anything else started with the container keep the value from `ddev start` until you run `ddev restart`.
- If the name is not set on the host when you run the command, the command keeps the value from `ddev start`.
- A name that a `.ddev/.env*` file also sets for `web` keeps the value from that file, as it does at start, because env files come later in the [override order](<#override-order>).

To set these from the command line, use [`ddev config`](<https://ddev.github.io/ddev/en/stable/users/usage/commands/#config>). Use `--web-environment` instead of `--web-environment-add` to replace the existing list rather than add to it:

```
# Set MY_ENV_VAR for the project
ddev config --web-environment-add="MY_ENV_VAR=someval"

# Set MY_ENV_VAR globally
ddev config global --web-environment-add="MY_ENV_VAR=someval"
```

## Host Variables Forwarded on Exec

Some variables describe the session that runs a command rather than the project, so DDEV copies them from the host into every [`ddev exec`](<https://ddev.github.io/ddev/en/stable/users/usage/commands/#exec>), [`ddev ssh`](<https://ddev.github.io/ddev/en/stable/users/usage/commands/#ssh>), and custom command, in any service. They reach only that command, so none of them needs a restart. A variable is forwarded when it is set on the host, even to an empty value, because some tools only check that it exists:

- The variables AI coding agents use to identify themselves, such as `AI_AGENT`, `CLAUDECODE`, `CURSOR_AGENT`, or `GEMINI_CLI`, so tools like PHPStan in the container know an agent called them. The list follows [laravel/agent-detector](<https://github.com/laravel/agent-detector#supported-agents>), minus `COPILOT_GITHUB_TOKEN`, which is a credential.
- In an interactive session, `TERM` and the variables a terminal identifies itself with, such as `COLORTERM` and `TERM_PROGRAM`, so programs know how many colors and which hyperlinks they can use.
- For [`ddev composer`](<https://ddev.github.io/ddev/en/stable/users/usage/commands/#composer>), [`COMPOSER_NO_BLOCKING`](<https://getcomposer.org/doc/03-cli.md#composer-no-blocking>).

None of them reach the daemons that `ddev start` launches, like `nginx`, `php-fpm`, or [`web_extra_daemons`](<https://ddev.github.io/ddev/en/stable/users/extend/customization-extendibility/#running-extra-daemons-in-the-web-container>), so a project runs the same whether a person or an AI agent started it.

To forward another variable to `web`, add its bare name to [`web_environment`](<#web_environment>).

## Applying Changes

A running container never picks up a new value on its own, though `ddev exec` reads each [bare `web_environment` name](<#web_environment>) from the host again. After adding or editing any `.ddev/.env*` file, its global equivalent, or a `web_environment` value, run:

```
ddev restart
```

An application `.env` file in the project root is the exception, since your framework reads it on every request rather than at container start.

## `ddev dotenv`

The [`ddev dotenv`](<https://ddev.github.io/ddev/en/stable/users/usage/commands/#dotenv>) commands create and update these files without an editor, quoting values correctly. Flags become variable names: `--api-url` writes `API_URL`.

| Command | Type | Where the file lands |
| --- | --- | --- |
| [`ddev dotenv set`](<https://ddev.github.io/ddev/en/stable/users/usage/commands/#dotenv-set>), [`ddev dotenv get`](<https://ddev.github.io/ddev/en/stable/users/usage/commands/#dotenv-get>) | project | The path is relative to the project root, so `.env` for an application file and `.ddev/.env.web` for a DDEV one |
| [`ddev dotenv global set`](<https://ddev.github.io/ddev/en/stable/users/usage/commands/#dotenv-global-set>), [`ddev dotenv global get`](<https://ddev.github.io/ddev/en/stable/users/usage/commands/#dotenv-global-get>) | global | The same `.ddev/<file>` path, resolved in the [global DDEV directory](<https://ddev.github.io/ddev/en/stable/users/usage/architecture/#global-files>), DDEV v1.25.4+ |

A DDEV env file is named `.ddev/.env*` in both scopes, so the only difference between the two commands is the word `global`:

```
ddev dotenv set .ddev/.env.redis --redis-tag 7-bookworm
ddev dotenv get .ddev/.env.redis --redis-tag

ddev dotenv global set .ddev/.env.redis --redis-tag 7-bookworm
ddev dotenv global get .ddev/.env.redis --redis-tag
```

## See Also

- [Using Add-ons](<https://ddev.github.io/ddev/en/stable/users/extend/using-add-ons/#customizing-add-on-configuration>) for configuring an add-on this way
- [Creating Add-ons](<https://ddev.github.io/ddev/en/stable/users/extend/creating-add-ons/index.md>) for shipping env files with an add-on
- [Custom Docker Compose Services](<https://ddev.github.io/ddev/en/stable/users/extend/custom-docker-services/index.md>) for using these variables in your own service
- [Environment Variables Provided](<https://ddev.github.io/ddev/en/stable/users/extend/custom-commands/#environment-variables-provided>) for the `DDEV_*` variables DDEV sets itself
