Environment Variables and Secrets
Once your Compose file describes services, networks, and volumes, the next question is: how do you configure those services without hard-coding values into the file?
A database password, application mode, or connection setting may need to change between environments. You don’t want to rewrite the Compose file every time. This is where environment variables and secrets become useful.
Why Externalize Configuration?
Consider a service that needs an application mode:
services:
app:
image: alpine
command: ["sh", "-c", "echo APP_MODE=$APP_MODE; sleep infinity"]
environment:
APP_MODE: productionThe value is directly inside compose.yaml.
That works, but now the configuration and the value are tied together:
compose.yaml
│
└── APP_MODE = productionWhat if you want:
developmenton your development machine and:
productionsomewhere else?
You would have to change the Compose file.
A better approach is to keep the Compose structure stable and provide the value from outside:
compose.yaml
│
│ expects
▼
APP_MODE
▲
│
external configurationThis gives you a useful separation:
The Compose file describes how the application is built. Configuration values can be supplied separately.
How Do Environment Variables Work In Compose?
Let’s start with the simplest form.
Create a directory:
mkdir compose-environment
cd compose-environmentCreate compose.yaml:
services:
app:
image: alpine
command: ["sh", "-c", "echo APP_MODE=$APP_MODE; sleep infinity"]
environment:
APP_MODE: developmentStart it:
docker compose upThe container prints:
APP_MODE=developmentThe environment section creates an environment variable inside the container.
Conceptually:
Compose configuration
│
▼
environment
│
▼
container
│
└── APP_MODE=developmentThis is the same environment-variable concept you already know from Docker containers. Compose is simply providing a convenient way to configure it.
How Can The Value Come From Your Shell?
Instead of writing the value directly in the Compose file, you can reference an environment variable:
services:
app:
image: alpine
command: ["sh", "-c", "echo APP_MODE=$APP_MODE; sleep infinity"]
environment:
APP_MODE: ${APP_MODE}Now Compose expects APP_MODE to be available as configuration.
Set it in your shell:
export APP_MODE=developmentThen start the application:
docker compose upThe container receives:
APP_MODE=developmentChange the value:
export APP_MODE=testingRun Compose again:
docker compose upNow the container receives:
APP_MODE=testingThe Compose file did not change. Only the configuration value changed.
This is the important pattern:
Same compose.yaml
│
├── APP_MODE=development → development
│
└── APP_MODE=testing → testingWhat Is A .env File?
Setting variables manually in the shell works, but you may want project-specific configuration stored in a file.
Create a .env file next to compose.yaml:
touch .envPut this inside it:
APP_MODE=developmentYour directory now looks like:
compose-environment/
├── compose.yaml
└── .envKeep the Compose file using:
services:
app:
image: alpine
command: ["sh", "-c", "echo APP_MODE=$APP_MODE; sleep infinity"]
environment:
APP_MODE: ${APP_MODE}Now run:
docker compose upCompose can read APP_MODE from the .env file and substitute it into the Compose configuration.
The result is still:
APP_MODE=developmentChange .env:
APP_MODE=testingRun:
docker compose upNow:
APP_MODE=testingThis is convenient for ordinary configuration values.
What Should Go Into .env?
A .env file is useful for configuration values that are not sensitive.
For example:
APP_MODE=development
APP_PORT=8080These values might legitimately vary between environments.
But be careful with secrets.
A database password such as:
DB_PASSWORD=super-secret-passwordis sensitive information.
Putting it in a .env file does not magically make it a secret.
The file is still an ordinary file containing the value.
Warning
Environment variables are useful for configuration, but don’t treat a .env file as a secure secret store. Sensitive credentials should be handled as secrets rather than casually stored in project configuration.
Environment Variables And Secrets Solve Different Problems
This distinction is important.
An environment variable answers:
“What configuration value should this service receive?”
A secret answers:
“What sensitive value does this service need without exposing it as ordinary configuration?”
Think of the difference as:
Configuration
│
└── environment variables
│
├── APP_MODE
├── APP_PORT
└── other ordinary settings
Sensitive configuration
│
└── secrets
│
├── password
├── API credential
└── other sensitive valuesBoth are configuration, but they have different sensitivity requirements.
Why Are Secrets More Secure?
Environment variables show up everywhere — processes, docker inspect output everywhere. But this is not the case with secrets. Secrets are generally secure because:
- Secrets are not exposed in environment variables, process lists or
docker inspectoutput - Secrets are mounted on
/run/secretsand only accessible to the service which you explicitly grant the access to
How Do Compose Secrets Work?
Compose lets you declare secrets separately from ordinary environment variables.
Here’s a small example.
Create compose.yaml:
services:
app:
image: alpine
command: ["sh", "-c", "echo 'Secret is available at:'; cat /run/secrets/app_password; sleep infinity"]
secrets:
- app_password
secrets:
app_password:
file: ./app_password.txtNow create the secret file:
printf '%s\n' 'my-demo-password' > app_password.txtStart the application:
docker compose upThe container can read the secret from:
/run/secrets/app_passwordThe important difference is that the secret is provided through the Compose secrets mechanism rather than being written directly into the service’s ordinary environment configuration.
The relationship is:
app_password.txt
│
▼
Compose secret
│
▼
container
│
▼
/run/secrets/app_passwordWhy Is The Secret Mounted As A File?
A Compose secret is made available to the service as a file.
In the example:
/run/secrets/app_passwordis the location inside the container.
The application can read that file when it needs the secret.
This is different from:
APP_PASSWORD=my-demo-passwordwhere the sensitive value is supplied as an environment variable.
The conceptual difference is:
Environment variable:
container
│
└── APP_MODE=development
Secret:
container
│
└── /run/secrets/app_password
│
└── sensitive valueThis separation makes it clear which values are ordinary configuration and which values are sensitive.
A Small Example With Both
A realistic service may need both ordinary configuration and a secret.
For example:
services:
app:
image: alpine
command:
- sh
- -c
- |
echo "Mode: $APP_MODE"
echo "Password file: /run/secrets/app_password"
cat /run/secrets/app_password
sleep infinity
environment:
APP_MODE: ${APP_MODE}
secrets:
- app_password
secrets:
app_password:
file: ./app_password.txtCreate .env:
APP_MODE=developmentCreate the secret file:
printf '%s\n' 'my-demo-password' > app_password.txtStart it:
docker compose upThe application receives:
APP_MODE=developmentthrough an environment variable, while the password is available through:
/run/secrets/app_passwordThe distinction is intentional:
app service
│
┌──────────┴──────────┐
│ │
environment secret
│ │
APP_MODE app_password
│ │
▼ ▼
ordinary sensitive
configuration configurationShould You Put Secrets In The Compose File?
Avoid writing the actual sensitive value directly into the Compose file.
For example, don’t do this:
services:
app:
image: alpine
environment:
APP_PASSWORD: my-demo-passwordThe password is now part of the Compose configuration itself.
Instead, the Compose file can describe where the secret comes from:
secrets:
app_password:
file: ./app_password.txtThe actual value is kept separately.
For this tutorial, the secret file is intentionally simple so you can see how the mechanism works. In a real production environment, secret management may involve a dedicated secret-management system.
What About .env And Secrets?
It helps to keep their roles separate:
| Mechanism | Typical use |
|---|---|
environment | Pass ordinary configuration into a container |
.env | Provide values used by Compose configuration |
secrets | Provide sensitive values to services |
| Secret file | Source of a Compose secret in this example |
The important thing is not to memorize every syntax detail.
Remember the responsibility of each mechanism.
.env
│
└── ordinary configuration values
secrets
│
└── sensitive values
services
│
├── environment
└── secretsA Useful Mental Model
You can now think of a Compose application as having several kinds of configuration:
compose.yaml
│
┌───────────────┼───────────────┐
│ │ │
services networks volumes
│
│
└──────── configuration ────────┐
│
┌────────────────┴──────────────┐
│ │
environment secrets
│ │
ordinary values sensitive valuesThis is a useful progression from the previous lessons:
services → what runs
networks → how services communicate
volumes → where persistent data lives
environment/secrets → how services are configuredCompose is gradually becoming a description of the whole application rather than just a command for starting containers.
Cleanup
Stop and remove the Compose application:
docker compose downRemove the example directory when you’re finished:
cd ..
rm -rf compose-environmentWarning
The example directory contains the demo secret file. Removing the directory deletes that local copy. In a real project, also make sure sensitive files are not accidentally committed to source control.
What You Should Remember
Environment variables and secrets are both ways of providing configuration, but they serve different purposes.
Use ordinary environment configuration for values such as:
APP_MODE=developmentUse secrets for sensitive values such as passwords.
The mental model is:
Compose Application
│
▼
Configuration
/ \
/ \
environment secrets
│ │
ordinary values sensitive valuesThe key idea is:
Don’t hard-code every environment-specific value into the Compose file. Describe the configuration structure once, then provide the values separately.
In the next lesson, you’ll look at a different Compose feature: reusable top-level elements using the x- prefix. This lets you reduce repetition when several services share configuration.