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'

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.

Docker Docker Swarm
Comments
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).
Mark Sweeter

I don't know if this is the BEST solution available.. but.. it DOES work. And I spent a lot of time trying to figure out how to do this. Glad I ran into this.

Zaramudin Abdul Azit

Hi, I'm new to docker secrets and just wondering how would you backup secrets. Is this something that you can export and deploy on another swarm?

Mark Sweeter

You can't.. actually. You can't edit them either. It's like a token (in AWS as an example). You can create it once. If you want to edit or set a new one, you have to just delete it and recreate.

Matthias Noback

I have no idea actually, sorry.

Luiz

Hi, thanks, it really saved some time. An complement, to run redis-server as 'redis' user:

command: [ "/bin/sh", "-c", "exec /usr/local/bin/gosu redis /usr/local/bin/redis-server --requirepass \"$$(cat /run/secrets/db_password)\""]

Matthias Noback

Cool, thanks for sharing.

Raj Nath

Very Impressive Docker tutorial. The content seems to be pretty exhaustive and excellent and will definitely help in learning Docker. I'm also a learner taken up Docker training and I think your content has cleared some concepts of mine. While browsing for Docker tutorials on YouTube i found this fantastic video on Docker. Do check it out if you are interested to know more.:-https://www.youtube.com/wat...

Sadok

Thank you for this great article, I come to similar issue when I need to read from Docker secrets values to create a service in a php app that uses Symfony DI, currently only variable environment can be get in the configuration file:


db_password: '%env(DATABASE_PASSWORD)%'


The good news is coming from this merged PR: https://github.com/symfony/...
where it allows to read from a file (among with other options) such docker secrets,


db_password: '%env(file:/run/secrets/db_password)%'