About Using Docker Config

TL;DR

What about using a Docker config instead of creating an image with an embedded configuration ?

Embedding configuration in an image ?

We often see Dockerfile like the following one where a new image is created only to add a configuration to a base image.

$ cat Dockerfile
FROM nginx:1.13.6
COPY nginx.conf /etc/nginx/nginx.conf

In this exemple, the local nginx.conf configuration file is copied over to the nginx image’s filesystem in order to overwrite the default configuration file , the one shipped in /etc/nginx/nginx.conf.

One of the main drawbacks of this approach is that the image needs to be rebuilt if the configuration changes.

Docker config enters in the picture

configs are available for services since Docker 17.06. Where secrets exist to store sensitive information, configs allows to store non-sensitive information, such as configuration files, outside a service’s image.

As for the other Docker primitives (container, image, volume, …) config has its own set of commands in the CLI.

$ docker config --help
Usage: docker config COMMAND
Manage Docker configs
Options:
Commands:
create Create a configuration file from a file or STDIN as content
inspect Display detailed information on one or more config files
ls List configs
rm Remove one or more configuration files
Run ‘docker config COMMAND — help’ for more information on a command.

Create a config

Coming back to the previous example, instead of copying the nginx configuration file in an image, we will create a Docker config out of it.

In this exemple, the nginx.conf file contains the following content.

user www-data;
worker_processes 4;
pid /run/nginx.pid;
events {
worker_connections 768;
}
http {
upstream api {
server api;
}
  server {
listen *:8000;
    location = /api {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
rewrite /api/(.*) /$1 break;
proxy_pass http://api;
}
}
}

It basically defines a web server which listens on port 8000 and forwards all the HTTP request arriving on the /api endpoint to the upstream api server.

Using the Docker CLI, we can create a config from this configuration file, we name this config proxy.

$ docker config create proxy nginx.conf
mdcfnxud53ve6jgcgjkhflg0s

We can then inspect the config as we would do with any other Docker primitives:

$ docker config inspect proxy
[
{
"ID": "x06uaozphg9kbnf8g4az4mucn",
"Version": {
"Index": 2723
},
"CreatedAt": "2017–11–21T07:49:09.553666064Z",
"UpdatedAt": "2017–11–21T07:49:09.553666064Z",
"Spec": {
"Name": "proxy,
"Labels": {},
"Data": "dXNlciB3d3ctZGF0YTsKd29y...ogIgICAgIH0KICAgIH0KfQo="
}
}
]

The data are only base64 encoded and can be easily decoded.

$ docker config inspect -f '{{json .Spec.Data }}' proxy | cut -d'"' -f2 | base64 -D
user www-data;
worker_processes 4;
pid /run/nginx.pid;
events {
worker_connections 768;
}
http {
upstream api {
server api;
}
server {
listen *:8000;
location = /api {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
rewrite /api/(.*) /$1 break;
proxy_pass http://api;
}
}
}

Per definition, the data within a config is not confidential and thus not encrypted.

Use a Config

Now that we have created the proxy config, we will see how we can use it in services. To do so, we will consider 2 approaches: using the command line and using a Docker stack.

In both cases, we define 2 services:

  • api: a simple http server listening on port 80 and sending back a suggestion of a city to visit each time it receives a Get request
  • a proxy using the config created above and sending all traffic targetting the /api endpoint to the upstream api service

We’ll test this setup by sending a HTTP Get request on the /api endpoint on the port 8000 of the proxy and making sure we get a response coming from the api.

Using the command line

We start by creating an overlay network. We will use this one so both services api and proxy can communicate together.

$ docker network create --driver overlay front

Then we create the api service.

$ docker service create --name api --network front lucj/api

The last step is to create the proxy service.

$ docker service create --name proxy \
--name proxy \
--network front \
--config src=proxy,target=/etc/nginx/nginx.conf \
--port 8000:8000 \
nginx:1.13.6

Once everything is in place, let’s send an HTTP Request on localhost, port 8000 (the port published on the host by the proxy service)

$ curl localhost:8000/api
{“msg”:”c249837f1f58 suggests to visit Emosiba”}

The request hits the proxy service which forwards it to the api. In other words, the nginx configuration file provided as a Docker config was correctly taken into account.

Using a Docker Compose file

Of course, using a Compose file to define the application and deploy it as a Docker stack is more convenient. We then create a stack.yml file with the following content.

version: "3.3"
services:
proxy:
image: nginx:1.13.6
configs:
- source: proxy
target: /etc/nginx/nginx.conf
ports:
- 8000:8000
networks:
- front
deploy:
restart_policy:
condition: on-failure
api:
image: lucj/api
networks:
- front
deploy:
restart_policy:
condition: on-failure
configs:
proxy:
external: true
networks:
front:

Note: as the config is created prior to run the application, it is defined as external in this file.

The application can then be run with the following command:

$ docker stack deploy -c stack.yml test
Creating service test_proxy
Creating service test_api

Let’s send a HTTP Get request to the /api endpoint of the proxy service.

$ curl localhost:8000/api
{“msg”:”f462d568c0b0 suggests to visit Onitufdu”}

Same as before, the request is forwarded to the api service.

Service update

When the content of a configuration needs to be modified, it’s a common pattern to create a new config (using docker config create) and then to update the service order to remove the access to the previous config and to add the access to the new one. The service command --config-rm and --config-add are

Let’s create a new config from the nginx-v2.conf file

$ docker config create proxy-v2 nginx-v2.conf
xtd1s1g6b5zukjhvup5vi4jzd

We can then update the service with the following command, doing so we remove the config named proxy and add the one named proxy-v2.

$ docker service update --config-rm proxy --config-add src=proxy-v2,target=/etc/nginx/nginx.conf proxy

Note: by default, when a config is attached to a service, it is available in the /config_name file, we then need to explicitly define the location using the target option.

Summary

Config is a pretty neat thing which helps to decouple the application with it’s configuration. Are you already using config in your application ?