Managing Secrets in Docker Compose — A Developer’s Guide
Photo by Shuttersnap on Unsplash
Introduction
It’s truly remarkable how much the direction of software engineering is dictated by inertia. Brendan Eich in 1995 designed JavaScript to be a client side scripting language of choice for the Netscape browser, over the years it has evolved to be on the client, server and other technologies like serverless. Similarly, Docker Compose has evolved from a local development tool into a popular choice for deploying applications, even in production environments. While Docker has published guidelines for using Compose in production, one critical aspect often overlooked by users is secure secret management.
In this guide, we’ll explore the best practices for managing secrets in modern Docker Compose deployments and discuss common pitfalls to avoid. We’ll progressively build up from basic approaches to more secure configurations.
The Problem with Environment Variables
Most Docker Compose setups handle secrets in one of two ways: either by hardcoding them directly in the compose file or using a .env
file:
While convenient, this approach has several security implications:
- Environment variables are accessible to all processes in a container
- They often appear in logs during debugging
- They can be exposed through application errors
- They make it difficult to maintain separation of concerns between services
Let’s demonstrate why this is problematic. With a basic Postgres container running:
We can easily inspect all environment variables:
We can also directly print all environment variables from within the container:
This exposure of secrets through environment variables has led to numerous security incidents over the years:
Although the first two examples presume an application misconfiguration and most modern web application frameworks try their best to censor secrets in error logs, this can be a pretty serious issue.
Better Secret Management with Docker Compose
Let’s explore three progressively more secure approaches to managing secrets in Docker Compose.
Prerequisites
First, ensure you’re running Docker Compose version 2.30.0 or later for full secrets support:
Your application should also be configured to read secrets from files rather than environment variables. Here’s a pattern we recommend for python, as an example:
This pattern:
- Prioritizes reading secrets from files using the
_FILE
suffix convention - Maintains compatibility with environment variables as a fallback
- Follows conventions used by official images like MySQL and Postgres
Example:
- For a given secret
POSTGRES_PASSWORD
check ifPOSTGRES_PASSWORD_FILE
environment variable containing a file path exists, if yes – read thePOSTGRES_PASSWORD
secret from the file - Else, read the secret from the
POSTGRES_PASSWORD
environment variable
For reference you can go through our Python implementation for Django and TypeScript for Next.js.
Approach 1: Environment Variables – Mount secrets inside your containers based on the values of host environment variables
The following implementation uses Docker Compose’s secrets feature to read environment variables from the host and mount them as files via a virtual filesystem in each of your services:
This mounts secrets as read-only virtual filesystem under /run/secrets/
:
Advantages:
- Easy setup – Simply mount the values of environment variables in memory as a filesystem
- Secrets never written to disk – Secrets remain in memory, reducing attack surface from filesystem access
- Better isolation between services – Each service only receives the secrets provisioned to it
- Read-only mounting – Prevents accidental or malicious corruption of secrets by containers
Disadvantages:
- Secrets exposed as host environment variables – Secrets must exist as environment variables on the host system – This can be addresses by using runtime secret injection via a secret manager such as Phase.
- World-readable within container – Any user, user group or process within the container can read the secrets (addressed in the next section)
- Requires service restart for updates – Changes to secrets require restarting affected services to take effect
Using things like export
on your host system to set secrets as environment can create other unwanted externalities like your secrets getting logged in your shell history. You can use the Phase CLI to to improve the overall secret management workflow by injecting secrets directly inside the docker compose process during runtime. Here’s an example:
Approach 2: File-Based Secrets – Mount secrets on the host system inside your container
The following implementation uses Docker Compose’s secrets feature to mount files containing secrets from the host in each of your services:
Advantages:
- Better isolation between services – Each service only receives the secrets provisioned to it
- Dynamic secret updates without restarts – Services can read updated secrets without container restarts
- Inherits host file permissions – Secret files maintain their permission attributes from the host system
- Read-only mounting – Prevents accidental or malicious corruption of secrets by containers
Disadvantages:
- Secrets written to disk on the host system – Creates potential security risk from filesystem access or backups
- Requires secure file management – Additional operational overhead to secure secret files
- World-readable by default – All users/processes in container can read secrets unless explicitly restricted – Addressed in the next section
To make creation of secrets on the host system easier and to improve the overall secret management workflow you can use the Phase CLI. Here’s an example:
Controlling access to secrets supplied to your services
Now that we have figured out how to supply secrets securely to your services, next let’s take a look how at how we can better protect them once provisioned inside our containers:
Docker Compose supports what they call a ‘long syntax’ for declaring how secrets are provisioned and controlling their access with more granularity within the respective service’s containers.
source
: The name of the secret as it exists on the platform.target
: The name of the file to be mounted in /run/secrets/ in the service’s task container, or absolute path of the file if an alternate location is required. Defaults to source if not specified.uid
andgid
: The numeric UID or GID that owns the file within /run/secrets/ in the service’s task containers. Default value is USER running container.mode
: The permissions for the file to be mounted in /run/secrets/ in the service’s task containers, in octal notation. The default value is world-readable permissions (mode 0444). The writable bit must be ignored if set. The executable bit may be set.
You can find the uid
and the gid
of for a given image by looking at the Dockerfile or if it’s your own image add one.
Here’s a postgresql example:
You can use the Unix file permission calculator to generate a suitable mode in octal notation: https://wintelguy.com/permissions-calc.pl
This configuration:
- Restricts secret access to specific users/groups
- Prevents other users from reading secrets
- Maintains write protection
You can verify the permissions:
For more information on the Docker Compose secrets long syntax, please see the Docker docs
Closing thoughts
While this is a good start for your docker compose secrets, below are some of the things that you should most consider when dealing with informational fissile materials like secrets:
-
Keep secrets away from source code, container files and images. Ivory and ebony, AC/AD and secrets and source code and container images; never the two shall meet.
-
Control access to secrets and keep them in sync and up to date with the rest of your team and deployments securely. Please do not add secrets to your git repository, drop your .env files over Slack or add them to your Notion docs as part of getting started with a project.
-
Don’t reuse secrets across different environments. Your production database password should never be the same as the pa$$w0rd@123 that you are using for local development. Compromise of one will mean de-facto compromise of all.
-
Encrypt secrets at all times, whether they are in flight over a network making they way to your production deployment or waiting in a database patiently to be pulled. “Dance like no one is watching, but encrypt like everyone is” – Werner Vogels, Chief Technical Officer Amazon.
-
Keep track of all changes and actions over your secrets. It’s 4:30 in the morning, do you know where your secrets are? Keep tabs on the who, what, when, and where so you can infer the why if and when there is an incident. Given the outsized number of breaches that occur due to a secret compromise, you will need those audit logs during an investigation.
Some or all of these points may seem obvious to most of you reading this, but what may not be as obvious is how tricky and tedious it can be to follow security best practices without losing development velocity. Consider using open-source secrets management platform like Phase which can help streamline the process. We are a bit biased plugging our own solution, but we think you’ll find it useful.
Conclusion
Docker Compose’s secret management capabilities have matured significantly, offering features typically found in larger container orchestration systems. While there are still some areas of improvements and limitations around permission enforcement (see docker/compose#12362), the available options provide a solid foundation for securing secrets in both development and smaller production environments.