Making a Docker image ready for use with Swarm Secrets

Posted on by 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'

        image: redis
        command: redis-server --requirepass $(cat /run/secrets/db_password)
            - 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 /usr/local/bin/

And the script mentioned in it could be something like this:

#!/usr/bin/env sh -eux

# Read the password from the password file

# Forward to the entrypoint script from the official redis image 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.

Docker Docker Swarm