How To Add Secrets To Your Docker Containers

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