Docker-compose and user ID

Many articles mention that there is one more way to run a container as a different user: using docker-compose, a tool that instruments Docker on how to run our containers. In short: instead of command line parameters, we use a structured config file that can look like this:

version: '3'
 services:
  my_service:
     image: my_image 
     command: /bin/bash

We can start our service by running:

$ docker-compose run my_service

Which is equivalent to:

$ docker run -it my_image /bin/bash

Specifying a UID is just a one more line in the config file:

version: '3'
 services:
  my_service:
     image: my_image
     user: $UID:$GID
     command: /bin/bash

Unfortunately, bash does not set GID by default, so we need to do it before running docker-compose. Using the id command inside a config file won’t work as the file is not pre-processed in any way.

$ GID=$(id -g) docker-compose run my_service
$$ id
uid=1000 gid=1000 groups=1000

Conclusion

Docker-compose is a nice tool that wraps docker run command and allows to make a configuration part of the project. It can be useful when creating build environments, automated tests, or complex run configurations.

How to set user when running a Docker container

I the previous post we created a C++ 20 app builder image. It works, but it has one very annoying feature: all created output files are owned by the root user.

$ ls -all
-rw-r--r-- 1 root root 13870 okt 18 20:06 CMakeCache.txt
drwxr-xr-x 5 root root 4096 okt 18 20:06 CMakeFiles
-rw-r--r-- 1 root root 1467 okt 18 20:06 cmake_install.cmake
-rw-r--r-- 1 root root 5123 okt 18 20:06 Makefile
-rwxr-xr-x 1 root root 54752 okt 18 20:06 opencv_hist

The reason is that by default, a docker container is running as root, and all operations inside are executed on its behalf. And yes, you can use a docker to bypass the root restrictions on your host machine (if not running in rootless mode).

$ echo "only root can read me" > secret_file
$ chmod 600 secret_file && sudo chown root:root secret_file
$ cat secret_file
cat: secret_file: Permission denied
$ docker run -v$PWD:/work -it my_image /bin/bash
$$ cat /work/secret_file
only root can read me

We don’t care about security for now – we just want to delete our object files without typing a password all the time.

Specifying user id

A simple trick is to use a Docker run command with a user argument. As you might guess, it allows you to specify the user that will be used when running the container. Interestingly, if you use a numeric ID, the user does not have to exist inside the container. Given UID will be just used in place of root, which allows us to this:

$ docker run --user "$(id -u):$(id -g)" -it my_image /bin/sh
$$ id
uid=1000 gid=1000 groups=1000
whoami
whoami: cannot find name for user ID 1000

As you can see the user 1000 does not really exist. For our simple build example, that is fine, but it can cause troubles for some operations.

Creating user

To create an additional user with a specific UID, we can add those 3 lines into the Dockerfile.

...
RUN addgroup --gid 1000 my_user
RUN adduser --disabled-password --gecos '' --uid 1000 --gid 1000 my_user
USER my_user
...

We disable the password and provide empty GECOS data (full name, phone number, etc.).

We can improve it a little by removing hard-coded ID numbers and using arguments instead.


ARG GROUP_ID
ARG USER_ID
...
RUN addgroup --gid $GROUP_ID my_user
RUN adduser --disabled-password --gecos '' --uid $USER_ID --gid $GROUP_ID my_user
USER my_user
...

Now we can pass our UID while building the image.

docker build  --build-arg GROUP_ID=$(id -g) --build-arg USER_ID=$(id -u) -t json_test .

Unfortunately, this means that anyone who wants to use our image will have to build it to make sure that her/his id matches the inside the container (my_user).