1. Overview
In this tutorial, we’ll learn how to switch users in a Docker image or container.
2. Why Do We Switch Users?
The user and group mechanism in Linux is a fundamental access control and security mechanism that allows files to be associated with specific users and groups. This allows us to restrict access to sensitive files to only authorized users and groups and prevent unauthorized access. As a result, it greatly reduces the surface of the attack in case of a security breach.
In the context of Docker, it’s considered a security best practice to avoid running containers as the root user. Since Docker containers share the same kernel with the host system, running as a root user exposes the entire system to potential attacks. Unfortunately, most base images are set to use the root user by default. To mitigate this risk, it’s recommended to always switch to a non-root user in Docker images. By doing so, we limit the potential damage of a security breach and keep our system secure.
3. Switching User When Building Docker Image
To build a Docker image, we write a series of instructions in the Dockerfile. Then, we run the docker build command to turn the Dockerfile into a Docker image. Throughout the steps, we sometimes want to change to a different user, be it temporarily or to set the default user of the Docker image. Within the Dockerfile, we can change the users we want to run the steps using the USER directive.
3.1. The USER Directive
The USER directive in the Dockerfile sets the user ID (UID) and, optionally, the group ID (GID) of the user to use for the subsequent steps in the build phase. There is no limit to how many times we can switch the user utilizing the USER directive. One common pattern is to temporarily switch the user to the root user for dependencies installation that requires escalation, then immediately switch back to a non-root user after that.
The USER directive in the Dockerfile accepts the name of the user as an argument and the optional argument for the group to associate the user with:
USER <user>[:<group>]
Additionally, the syntax also supports the UID and GID directly:
USER <UID>[:<GID>]
If the user doesn’t have a primary group and we don’t specify the group when using the USER directive, it’ll default to the root group.
3.2. Using the USER Directive
To demonstrate the working of this directive, let’s build a simple image based on the Alpine Linux base image with the default user as root:
$ cat Dockerfile
FROM alpine:3.18.0
WORKDIR /tmp
RUN touch file
CMD ls -lh .
Firstly, the Dockerfile specifies the base image as alpine:3.18.0, which is the Docker image of Alpine Linux, version 3.18.0. Then, we set the working directory to be at /tmp using the WORKDIR directive. Then, we run a shell command touch file to create an empty file. Finally, we use the CMD directive to run the ls -lh command whenever we run the Docker image.
Then, we can build the image using the docker build command:
$ docker build --tag simpleimage:latest .
Running the Docker image, we’ll see the output shows that the files belong to the root user and root group:
$ docker run --rm simpleimage:latest
total 0
-rw-r--r-- 1 root root 0 May 14 01:30 file
Let’s rewrite our Dockerfile now to use the USER directive to change the user to the appuser:
$ cat Dockerfile
FROM alpine:3.18.0
WORKDIR /tmp
RUN adduser -D appuser
USER appuser
RUN touch file
CMD ls -lh .
Firstly, we create the appuser in the container using the adduser command. Without first creating the user, the USER directive will fail because it can’t resolve the appuser to a valid UID:
unable to find user appuser: no matching entries in passwd file
Running the Docker image, we’ll see that now the file belongs to appuser:
$ docker run --rm simpleimage:latest
total 0
-rw-r--r-- 1 appuser appuser 0 May 14 01:56 file
In other words, after the USER directive changes the user to appuser, the Docker engine runs the RUN touch file command as the appuser.
3.3. Why RUN su Directive Doesn’t Work
One common mistake is to switch the user using the su command with the RUN directive in the Dockerfile. Specifically, we might mistakenly think that instead of using the USER directive, we can replace it with RUN sh to achieve the same outcome:
$ cat Dockerfile
FROM alpine:3.18.0
WORKDIR /tmp
RUN adduser -D appuser
RUN su appuser
RUN touch file
CMD ls -lh .
The intention of the above Dockerfile is to issue the first RUN su appuser to change into the appuser. After that, we would expect the remainder of the directives will run as the appuser user. However, we’ll see that the su appuser doesn’t get persisted across the image layer:
$ docker build . -q --tag simpleimage:switch-user-with-su
$ docker run --rm simpleimage:switch-user-with-su
total 0
-rw-r--r-- 1 root root 0 May 14 04:41 file
To understand why changing the user in a Docker image is not as simple as running the su command, it’s important to recognize how Docker builds an image. The Docker engine uses a series of docker run and docker commit commands to execute each step in the Dockerfile, starting each step as a new container and committing the changes as a new layer.
Furthermore, each RUN directive runs in a new shell and environment, so running su in a standalone RUN directive won’t have any effect on subsequent steps, which start with the default shell settings. Similarly, this also explains why changes made with cd and export commands won’t persist across layers.
However, directives like USER, WORKDIR, and ENV can modify the default shell settings for subsequent steps, including the RUN directives. This enables changes made with these directives to persist across layers. For this reason, it’s best to use these directives to change the user or group in a Docker image rather than relying on the su command.
If we still wish to use the su command to temporarily change the user for certain commands, one way we can workaround this issue is to pass the command we want to execute to the -c option of the su command:
$ cat Dockerfile
FROM alpine:3.18.0
WORKDIR /tmp
RUN adduser -D appuser
RUN su appuser -c 'touch file'
CMD ls -lh .
This would ensure the command touch file is run by the Docker engine as appuser while building the image.
4. Switching User for Docker Container
To run a Docker container as a different user, we can use the –user option of the docker run command.
For example, running the Alpine Linux image with the command whoami will give us the root username, which is the default user according to the image Dockerfile:
$ docker run --rm alpine:latest whoami
root
We can specify the –user option to start the same container with the guest user instead:
$ docker run --rm --user guest alpine:latest whoami
guest
Similarly, we can run commands on a running container using the –user option of the docker exec command:
$ docker exec --user guest -it alpine whoami
guest
Note that the users we are running as must exist in the /etc/passwd of the Docker container. Otherwise, the command will fail as it fails to resolve the username to a user entry in the /etc/passwd file:
$ docker exec --user unknown -it alpine whoami
unable to find user unknown: no matching entries in passwd file
5. Conclusion
In this tutorial, we’ve first looked briefly at the user and group access control mechanism in Linux. Then, we learned that we could switch users during the Docker image build phase and start the container. Changing the user during the build phase is as simple as using the USER directive in the Dockerfile. We’ve also seen how the su command is ineffective for changing users when building a Docker image.
Finally, we’ve learned that both the docker run and docker exec command support the –user option for changing users.