Use Unix sockets with Docker

By default, you are supposed to use TCP sockets to communicate with your applications running in Docker. But what if you want to use Unix sockets instead? The answer is you can: you make the application create the socket file in a volume and set the proper permissions to it. You can them use it for your webserver to talk to your application or for cross container communications. The tricky part is to set the proper permissions on the socket.

Let's say you are creating a Python web application running in a container with UWSGI. It will run in the container as the uwsig user. You want it to use a Unix socket to communicate with your nginx webserver running on the host under the nginx user. You need to the socket:

  • To belong to the uwsgi user inside the container so your application can use it.
  • To belong to the nginx user outside it, so nginx can use it.

The question is, how do we fullfil these two requirements? First, let's explain how file ownership works.

In Unix each file has a owner and a group. You can see them with ls -l:

$ ls -l
-rw-rw-r--. 1 jujens jujens 366 Sep 12 14:13 README.md

Here we have a file named README.md that belongs to a user named jujens and a group named jujens. What you must know is: the names are just the human readable version of these attributes. They are encoded in the filesystem as numbers respectively named uid and gid.

So what we need to do, is make sure that both the nginx user (in the host system) and the uwsgi user (in the container) have the same uid (and gid). Depending on where we do ls -l we'll get different human readable names but the permissions will be correct.

To do that, we need 3 commands:

  1. id to get the uid and gid of a user: id -u will give you the uid of the current user and id -g the gid of the current user. You can pass an optional username to get the uid and gid for a specific user: id -u nginx.
  2. usermod to change the uid of a user: usermod -u <NEW_ID> <USER>
  3. groupmod to change the gid of a user: groupmod -g <NEW_ID> <GROUP>

Now we just have to use them correctly. First, let's create a setup.sh script next to your Dockerfile. It will change the uid and gid for the uwsgi user inside the container. It takes two parameters, the uid and the gui:

 1 #!/usr/bin/bash
 2 
 3 # Quit on error.
 4 set -e
 5 # Treat undefined variables as errors.
 6 set -u
 7 
 8 
 9 function main {
10     local uwsgi_uid="${1:-}"
11     local uwsgi_gid="${2:-}"
12 
13     # Change the uid
14     if [[ -n "${uwsgi_uid:-}" ]]; then
15         usermod -u "${uwsgi_uid}" uwsgi
16     fi
17     # Change the gid
18     if [[ -n "${uwsgi_gid:-}" ]]; then
19         groupmod -g "${uwsgi_gid}" uwsgi
20     fi
21 
22     # Setup permissions on the run directory where the sockets will be
23     # created, so we are sure the app will have the rights to create them.
24 
25     # Make sure the folder exists.
26     mkdir /var/run/uwsgi
27     # Set owner.
28     chown root:uwsgi /var/run/uwsgi
29     # Set permissions.
30     chmod u=rwX,g=rwX,o=--- /var/run/uwsgi
31 }
32 
33 
34 main "$@"

In the Dockerfile, we need to:

  1. Define the variables we what to pass to the script.
  2. Copy the script in the container.
  3. Pass the correct variables to the script.

To do so, we need the lines below (note: this Docker file is not complete, I just present here the relevant lines for what we need to do):

# Define the variables available.
ARG uwsgi_uid
ARG uwsgi_gid

# Copy the script.
COPY setup.sh /

# Launch setup.sh
RUN bash /setup.sh "${uwsgi_uid}" "${uwsgi_gid}"

At this point, all we have to do is use this Dockerfile. I'll describe two ways to use it:

  1. With the docker command.
  2. With the docker-compose command. To learn more about docker-compose, see the documentation.

Directly with the docker command

First we need to build our container with the proper values in our uwsgi_uid and uwsgi_gid variables. To achieve that, we need to use the --build-arg option (note: the $(…) construct allows us to execute a command. $(…) will be replaced by the output of the command):

docker build --build-arg uwsgi_uid=$(id -u nginx) --build-arg uwsgi_gid=$(id -g nginx)

Now we can run the application by mounting /var/run/uwsgi (the folder in which the socket of our application will be) into a volume so it is accessible by the host:

docker run --volume "/var/run/myapp/:/var/run/uwsgi/:rw"

And that's it. If something is not clear, please leave a comment.

With docker-compose

First we need to create a docker-compose.yml file. We need to specify an args section inside build to define the variables and a volumes section inside myapp to define the volume:

 1 version: '2'
 2 services:
 3     myapp:
 4         build:
 5             context: .
 6             args:
 7                 uwsgi_uid: ${UWSGI_UID}
 8                 uwsgi_gid: ${UWSGI_GID}
 9         restart: always
10         volumes:
11         - ${SOCKET_DIR}/:/var/run/uwsgi/:rw

Then we need to export in our shell the UWSGI_UID, UWSGI_GID and SOCKET_DIR variables so docker-compose can find them.

export UWSGI_UID=$(id -u nginx)
export UWSGI_GID=$(id -g nginx)
export SOCKET_DIR="/var/run/myapp"

Finally, in the same shell, we can launch docker-compose:

docker-compose up -d

And that's it. If something is not clear, please leave a comment.


blogroll

social

>