This post is inspired by this SO thread.
Note: You must be running Docker in swarm mode, and docker-compose can’t use external secrets.
You might have heard that you should use Docker Secrets to manage sensitive data like passwords and API keys shared with containers. For example, you can set the default database admin password with the POSTGRES_PASSWORD
environment variable:
# docker-compose.yml
version: "3.9"
services:
postgresql:
image: postgres
environment:
POSTGRES_PASSWORD: P@ssw0rd!
But now anybody who can read docker-compose.yml
can read your password! To avoid this, it is common practice is to use secrets to set sensitive environment variables at runtime.
One way to create a secret is in the secrets
block of a docker-compose configuration. Then, we can add that secret to our services. By default, secrets will be mounted in the container’s /run/secrets
directory. We also need to set the environment variable POSTGRES_PASSWORD_FILE
to the path of our secret /run/secrets/postgres-passwd
. The postgres
container uses environment variables ending in _FILE
to set the corresponding sensitive environment variable, e.g. POSTGRES_PASSWORD
.
# docker-compose.yml
version: "3.9"
services:
postgresql:
image: postgres
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/postgres-passwd
secrets:
- postgres-passwd
secrets:
postgres-passwd:
file: ./secrets/postgres-passwd
That’s it! Now your container deployment will use a POSTGRES_PASSWORD
that isn’t in a docker-compose.yml
for all to see. The actual secret is in ./secrets/postgres-passwd
which can be protected with traditional file permissions. You can even randomly generate secrets instead of using files.
openssl rand 33 -base64 | docker secret create postgres-passwd -
What about containers that don’t support secrets?
Remember what I said about the postgres
container using environment variables ending in _FILE
? It uses a custom docker-entrypoint.sh script to do that. Specifically, it uses the file_env
function to set the specific environment variables it’s looking for. To add support for secrets to a container, create your own docker-entrypoint.sh
script with that file_env
function and call it to set the environment variables you want. The last line of your entrypoint script should be exec "$@"
so you can run whatever was originally the entrypoint as command parameters.
# docker-entrypoint.sh
#!/bin/bash
set -e
# usage: file_env VAR [DEFAULT]
# ie: file_env 'XYZ_DB_PASSWORD' 'example'
# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of
# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature)
file_env() {
local var="$1"
local fileVar="${var}_FILE"
local def="${2:-}"
if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
echo >&2 "error: both $var and $fileVar are set (but are exclusive)"
exit 1
fi
local val="$def"
if [ "${!var:-}" ]; then
val="${!var}"
elif [ "${!fileVar:-}" ]; then
val="$(< "${!fileVar}")"
fi
export "$var"="$val"
unset "$fileVar"
}
file_env 'POSTGRES_PASSWORD'
exec "$@"
In your docker-compose service configuration, mount the script and set the entrypoint
and command
values. This example uses the env
command so we can see our variables were set.
# docker-compose.yml
version: "3.9"
services:
test:
image: debian
volumes:
- ./docker-entrypoint.sh:/docker-entrypoint.sh
entrypoint: /docker-entrypoint.sh
command: env
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/postgres-passwd
secrets:
- postgres-passwd
secrets:
postgres-passwd:
file: ./secrets/postgres-passwd
Now deploy your service and see if it works!
docker-compose up