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 apply here:
- Project files, in the project directory and mostly under
.ddev. - Global files in the global DDEV directory, which apply to every project. This page writes that directory as
$HOME/.ddev, its usual location, but it can be somewhere else.
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 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* and $HOME/.ddev/.env* |
project 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 |
project 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, creating it if it doesn’t exist. Set 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, which is useful in a README or a CI script because it creates the file and updates single keys without disturbing the rest:
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:
| 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 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.
To see the result:
ddev utility check-custom-configlists the env files DDEV found underEnvironment, in the order they are applied.ddev utility compose-configshows the rendered.ddev/.ddev-docker-compose-full.yaml, including theenvironmentsection of each service.ddev exec envandddev exec -s <service> envshow what actually reached a running container.
Global Env Files¶
With DDEV v1.25.4+, the same filenames work in the global configuration directory, 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 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 for the full sequence.
Use ddev 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:
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 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:
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:
To set these from the command line, use ddev 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"
Applying Changes¶
A running container never picks up a new value on its own. After adding or editing any .ddev/.env* file, its global equivalent, or a web_environment value, run:
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 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, ddev 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, ddev dotenv global get |
global | The same .ddev/<file> path, resolved in the global DDEV directory, 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 for configuring an add-on this way
- Creating Add-ons for shipping env files with an add-on
- Custom Docker Compose Services for using these variables in your own service
- Environment Variables Provided for the
DDEV_*variables DDEV sets itself