Making a Docker image ready for use with Swarm Secrets
Matthias Noback
Here’s what I wanted to do:
- Run the official
redis
image as a service in a cluster set up with Docker Swarm. - Configure a password to be used by connecting clients. See also Redis’s
AUTH
command. The relevant command-line option when starting the redis server would be--requirepass
.
This is just a quick post, sharing what I’ve figured out while trying to accomplish all of this. I hope it’s useful in case you’re looking for a way to make a container image (an official one or your own) ready to be used with Docker Secrets.
I started out with this docker-compose.yml
configuration file, which I provided as an option when running docker stack deploy
:
version: '3.1'
services:
redis:
image: redis
command: redis-server --requirepass $(cat /run/secrets/db_password)
secrets:
- db_password
secrets:
db_password:
file: db_password.txt
This configuration defines the db_password
secret, the (plain text) contents of which should be read from the db_password.txt
file on the host machine. The (encrypted) secret will be stored inside the cluster. When the redis
service gets launched on any node in the cluster, Docker shares the (decrypted) secret with the container, by means of mounting it as a file (i.e. /run/secrets/db_password
) inside that container.
The naive solution above looked simple and I thought that it might just work. However, I got this error message:
Invalid interpolation format for "command" option in service "redis": "redis-server --requirepass $(cat /run/secrets/db_password)"
Docker Compose does variable substitution on commands and thinks that $(...)
is invalid syntax (it’s expecting ${...}
). I escaped the ‘$’ by adding another ‘$’ in front of it: redis-server --requirepass $$(cat /run/secrets/db_password)
. New errors:
Reading the configuration file, at line 2
>>> 'requirepass "$(cat" "/run/secrets/db_password)"'
Bad directive or wrong number of arguments
Bad stuff. I thought I’d just have to wrap the values into quotes: redis-server --requirepass "$(cat /run/secrets/db_password)"
. Now, everything seemed to be fine, the Redis service was up and running, except that the password wasn’t set to the contents of the db_password
. Instead, when I tried to connect to the Redis server, the password seemed to have become literally “$(cat /run/secrets/db_password)
”…
At this point I decided: let’s not try to make this thing work from inside the docker-compose.yml
file. Instead, let’s define our own ENTRYPOINT
script for a Docker image that is built on top of the existing official redis
image. In this script we can simply read the contents of the db_password
file and use it to build up the command.
The Dockerfile
would look something like this:
FROM redis:3.2.9-alpine
COPY override_entrypoint.sh /usr/local/bin/
ENTRYPOINT ["override_entrypoint.sh"]
And the override_entrypoint.sh
script mentioned in it could be something like this:
#!/usr/bin/env sh -eux
# Read the password from the password file
PASSWORD=$(cat ${REDIS_PASSWORD_FILE})
# Forward to the entrypoint script from the official redis image
docker-entrypoint.sh redis-server --requirepass "${PASSWORD}"
Building the image, tagging it, pushing it, and using it in my docker-compose.yml
file, I could finally make this work.
I was almost about to conclude that it would be smart not to try and fix everything in docker-compose.yml
and simply define a new image that solves my uses case perfectly. However, the advantage of being able to pull in an image as it is is quite big: I don’t have to rebuild my images in case a new official image is released. This means I won’t have to keep up with changes that make my own modifications break in some unexpected ways. Also, by adding my own entrypoint script, I’m ruining some of the logic in the existing entrypoint script. For example, with my new script it’s impossible to run the Redis CLI.
La grande finale
Then I came across some other example of running a command, and I realized, maybe I’ve been using the wrong syntax for my command. After all, there already appeared to be some kind of problem with chopping up the command and escaping it in unexpected ways. So I tried the alternative, array-based syntax for commands:
command: ["redis-server", "--requirepass", "$$(cat /run/secrets/db_password)"]
No luck. However, the problem was again that the password was taken literally, instead of being evaluated. I remember there was an option to provide a shell command as an argument (using sh -c
), to be evaluated just like you would pass in a string to eval()
. This turned out to be the final solution:
command: ["sh", "-c", "redis-server --requirepass \"$$(cat /run/secrets/db_password)\""]
I hope this saves you some time, some day.