Skip to content

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: production

The value is directly inside compose.yaml.

That works, but now the configuration and the value are tied together:

compose.yaml
     │
     └── APP_MODE = production

What if you want:

development

on your development machine and:

production

somewhere 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 configuration

This 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-environment

Create compose.yaml:

services:
  app:
    image: alpine
    command: ["sh", "-c", "echo APP_MODE=$APP_MODE; sleep infinity"]
    environment:
      APP_MODE: development

Start it:

docker compose up

The container prints:

APP_MODE=development

The environment section creates an environment variable inside the container.

Conceptually:

Compose configuration
        │
        ▼
    environment
        │
        ▼
     container
        │
        └── APP_MODE=development

This 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=development

Then start the application:

docker compose up

The container receives:

APP_MODE=development

Change the value:

export APP_MODE=testing

Run Compose again:

docker compose up

Now the container receives:

APP_MODE=testing

The 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     → testing

What 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 .env

Put this inside it:

APP_MODE=development

Your directory now looks like:

compose-environment/
├── compose.yaml
└── .env

Keep 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 up

Compose can read APP_MODE from the .env file and substitute it into the Compose configuration.

The result is still:

APP_MODE=development

Change .env:

APP_MODE=testing

Run:

docker compose up

Now:

APP_MODE=testing

This 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=8080

These values might legitimately vary between environments.

But be careful with secrets.

A database password such as:

DB_PASSWORD=super-secret-password

is 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 values

Both 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 inspect output
  • Secrets are mounted on /run/secrets and 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:

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

Now create the secret file:

printf '%s\n' 'my-demo-password' > app_password.txt

Start the application:

docker compose up

The container can read the secret from:

/run/secrets/app_password

The 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_password

Why 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_password

is the location inside the container.

The application can read that file when it needs the secret.

This is different from:

APP_PASSWORD=my-demo-password

where 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 value

This 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.txt

Create .env:

APP_MODE=development

Create the secret file:

printf '%s\n' 'my-demo-password' > app_password.txt

Start it:

docker compose up

The application receives:

APP_MODE=development

through an environment variable, while the password is available through:

/run/secrets/app_password

The distinction is intentional:

                 app service
                     │
          ┌──────────┴──────────┐
          │                     │
     environment              secret
          │                     │
      APP_MODE            app_password
          │                     │
          ▼                     ▼
      ordinary              sensitive
     configuration          configuration

Should 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-password

The 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.txt

The 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:

MechanismTypical use
environmentPass ordinary configuration into a container
.envProvide values used by Compose configuration
secretsProvide sensitive values to services
Secret fileSource 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
 └── secrets

A 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 values

This 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 configured

Compose 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 down

Remove the example directory when you’re finished:

cd ..
rm -rf compose-environment

Warning

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=development

Use secrets for sensitive values such as passwords.

The mental model is:

                 Compose Application
                         │
                         ▼
                    Configuration
                    /            \
                   /              \
          environment            secrets
               │                     │
       ordinary values        sensitive values

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

Last updated on