Configuration is messy
Working on a project with multiple environments and keeping track of all the configuration values is definitely not a simple task! Today, we need different settings for development, staging, and production. We might have API keys, database credentials, feature flags, and service URLs. Hardcoding these values is a nightmare, and sharing sensitive information in our codebase is a security risk.
The Original Story was published here, and it needs your support please:
Docker Compose How to Set Environment Variables
One might say that just changing the values manually before deployment is the solution to all our problems, but I beg to differ. Because every environment needs different credentials, every developer might work with different local settings, or we might need to rotate secrets without touching our code.
How can we keep track of these configurations? How can we collaborate with our teammates on the correct settings? Or share our work without exposing sensitive data?
Docker Compose environment variables to the rescue! 🦸♂️
What are environment variables anyway?
Before jumping to how Compose handles them, we have to understand what environment variables are. Environment variables are dynamic values that can affect the way running processes behave on a computer. They’re a fundamental part of how applications receive configuration without hardcoding values. Your application reads these variables at runtime, making the same code work differently in different environments.
So, environment variables are a unified way to configure services without changing the actual code. And Compose gives us multiple flexible ways to set these variables with proper separation between code and configuration. It’s a tool that makes managing environment-specific settings incredibly straightforward.
Compose comes down to understanding a few simple patterns that we can add to our repository so others or even our future-self will easily configure services, as simple as defining values in a file.
Installing Docker Compose
Mac users, if you have Docker Desktop For Mac installed, it comes bundled with Compose. Otherwise, install Docker Compose by following the official guide.
Simple use-case
Let’s get down to the nitty-gritty! As an example, we have a Node.js API service that needs database credentials, an API port, and a secret key for JWT tokens. That’s it, only a few configuration values.
version: "3.1"
services:
db:
image: postgres:11.6-alpine
environment:
- POSTGRES_DB=myapp
- POSTGRES_PASSWORD=dbpass123
ports:
- "5432:5432"
api:
image: node:14-alpine
environment:
- NODE_ENV=development
- PORT=3000
- DATABASE_URL=postgres://user:dbpass123@db:5432/myapp
- JWT_SECRET=my-super-secret-key
ports:
- "3000:3000"
depends_on:
- db
This is how our Compose YAML file should look like. The convention is to save it in the root folder of our project under the name docker-compose.yml.
Going directly to the environment property, we can define environment variables in a simple array format. Each line follows the pattern KEY=VALUE. These variables will be available inside the container when our service runs.
In our case, we set NODE_ENV to tell our application it's running in development mode. We define the PORT our server should listen on. We provide a full DATABASE_URL connection string that points to our db service (notice we use the service name db as the hostname), and lastly, we set a JWT_SECRET for token signing.
The cool thing is that our application can access these values through the standard environment variable interface. In NodeJS, for example, we can use process.env.PORT to read the port value. We don't need any special libraries or complex configuration systems. It's as simple as reading from the environment.
Once the file is ready and Docker Compose is installed, we can run docker-compose up in the root folder of our project. Our service will start with all these environment variables set and ready to use.
Now we have our API configured for local development with all the relevant settings inside.
Using .env files
version: "3.1"
services:
db:
image: postgres:11.6-alpine
env_file:
- .env
ports:
- "5432:5432"
api:
image: node:14-alpine
env_file:
- .env
ports:
- "3000:3000"
depends_on:
- db
# .env file
NODE_ENV=development
PORT=3000
DATABASE_URL=postgres://user:pass@db:5432/myapp
JWT_SECRET=my-super-secret-key
POSTGRES_DB=myapp
POSTGRES_PASSWORD=dbpass123
In real-life we might not want to expose all our configuration in the YAML file. For example, when working with sensitive credentials or when we have many environment variables, keeping them in the compose file becomes messy. In this example, we use an external .env file to store our variables.
Instead of the environment property, we use env_file and point to our .env file. This file follows a simple format where each line is a KEY=VALUE pair. Like we did in the inline approach, we define all our variables, but now they're separate from our Compose configuration.
The cool thing is that we can add .env to our .gitignore file to prevent sensitive data from being committed to our repository. We can then create a .env.example file with dummy values to show others what variables are needed without exposing real credentials.
See that we can pass an array to env_file? We could have multiple env files for different purposes - maybe .env.shared for common variables and .env.local for developer-specific overrides. When you specify a file with env_file, Compose loads those variables into your containers at runtime.
That’s it, now we can run docker-compose up to make sure our service loads all variables from the file. We don't have to know the actual values in advance. This can be a very complex process when managing dozens of variables, and it's all encapsulated by Docker Compose.
Advanced use-case
version: "3.1"
services:
db:
image: postgres:11.6-alpine
environment:
- POSTGRES_DB=myapp
- POSTGRES_PASSWORD=db-password-here
ports:
- "5432:5432"
api:
image: node:14-alpine
env_file:
- .env.shared
- .env.local
environment:
- NODE_ENV=development
- DATABASE_HOST=db
- DATABASE_PORT=5432
ports:
- "3000:3000"
depends_on:
- db
# .env.shared
NODE_ENV=development
LOG_LEVEL=debug
DATABASE_HOST=db
DATABASE_PORT=5432
# .env.local (gitignored)
DB_PASSWORD=super-secret-password
API_KEY=my-api-key-here
JWT_SECRET=another-secret-value
For more complex scenarios, we can combine multiple approaches. In this example, we use multiple env files to organize our configuration better.
Notice how we separate concerns? The environment section in the YAML contains values that are the same for everyone - like service names and ports within the Docker network. Meanwhile, our env files handle the actual configuration values that might differ between developers or environments.
We’re mixing inline environment variables with env files. The DATABASE_HOST is set inline in the YAML because it always points to our db service name within the Docker network. But variables like API_KEY and JWT_SECRET come from the env files, keeping sensitive data out of the compose file itself.
See that we have two env files? The .env.shared contains non-sensitive, common variables that we commit to the repository. The .env.local holds secrets and developer-specific overrides that we keep out of git. This separation makes collaboration easy while keeping credentials secure. Not only that, but if both files define the same variable, the last one loaded (.env.local) takes precedence, giving developers the power to override shared defaults.
And we’re done! Now you can easily manage configuration across different environments with your teammates or open-source contributors with Docker Compose environment variables.
Top comments (0)