Building a Docker image with customizable GID and UID
You might ask yourself why you would build yet another docker image that supports customizable GID and UID, and you are certainly right to ask. There are literally hundreds of images out there with the same capabilities. In this post I will answer two questions. Why you would want to run daemons inside the docker context as something that’s not root, but also why you should build your own image.
Reasons to abandon root #
If you hang around self hosting forums for some time you will undoubtedly run into a discussion about why you should run your daemons using a non-root user, and to my surprise, the most common reason people give for doing so seem to be security.
While there are valid security issues with running docker containers with the root user, these issues are almost exclusively related to running unknown or untrusted images. A spun-up image running unknown code or instructions could do nasty things to your bare metal system, but if you’re running trusted images (images you built yourself), this is less of an issue.
The reason to implement a custom user has more to do with bare metal user mapping, especially if you provide ways to customize the GID and the UID. These things come into play, especially when dealing with containers providing things like smb or nfs mounts.
Building a base image with a custom user #
In this post I’m going to refer to my own docker-alpine-base image. My own image has a few additional features, but it follows the same line of reasoning.
The end goal of this image is to provide a base image using Alpine Linux, that we can use to build more advanced images from. The base image will have a custom user to run our entrypoint
and this user has a GID and UID that can be customized through the use of environmental variables.
Since we are using Alpine Linux as a base, we start by adding the instruction to the top of our Dockerfile
FROM alpine:3
We will need to install some software in order to create a custom user (shadow) and to be able to execute as the newly created user (su-exec), so we make sure to do that, by adding:
RUN apk add --no-cache su-exec shadow
When this has been installed, we can add our desired user with:
RUN groupadd -o -g 1000 dockrun \
&& useradd -o -u 1000 -g 1000 -M -s /sbin/nologin dockrun
These two run instructions can also be combined into a single one, like this:
RUN apk add --no-cache su-exec shadow \
&& groupadd -o -g 1000 dockrun \
&& useradd -o -u 1000 -g 1000 -M -s /sbin/nologin dockrun
Okay, so far we have accomplished creating a custom user and we have the ability (su-exec) to execute things using this user, but how do we go about customizing the GID and the UID? The answer lies in the use of groupmod
and usermod
. Since we want to be able to change these with environmental variables, we need to run these commands every single time the image is spawned and the container starts. The only thing that will always run is the entrypoint
, so we add a custom one to our image with:
ENTRYPOINT ["/docker-entrypoint.sh"]
So now, every time the image is spawned into a container, a script called docker-entrypoint.sh
in the /
folder will be executed, but so far we haven’t created such a script, but now is the time to deal with it.
In our case, we need an entrypoint
that will allow us to send in any command to be executed. It does not need this, but it provides a nice entrypoint
that can be re-used by images built upon our base image, making it possible to skip the ENTRYPOINT
instructions when we create those images.
So we create the following script as the entrypoint
:
#!/usr/bin/env sh
PUID=${PUID:-$(id -u dockrun)} # Use the environment variable value or default (1000) if variable is not set.
PGID=${PGID:-$(id -g dockrun)} # Use the environment variable value or default (1000) if variable is not set.
groupmod -o -g "$PGID" dockrun # Modify the group id.
usermod -o -u "$PUID" dockrun # Modify the user id.
printf "User dockrun is running with the following IDs:\n"
printf "\tUID: %s\n" "${PUID}"
printf "\tGID: %s\n" "${PGID}"
exec su-exec dockrun "$@" # Run the command the entrypoint was called with as the custom user.
Now in order for the entrypoint
to be available, we add it to the Dockerfile
instruction, making the complete file look like this:
FROM alpine:3
RUN apk add --no-cache su-exec shadow \
&& groupadd -o -g 1000 dockrun \
&& useradd -o -u 1000 -g 1000 -M -s /sbin/nologin dockrun
COPY docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
Remember to make the script docker-entrypoint.sh
executable or it will fail to execute inside the container.
Finally, build your image with:
docker build -t <docker-image-tag:version> .
Once the build completes, you can run it with:
docker run -it --rm <docker-image-tag:version> id
And the output will be:
User dockrun is running with the following IDs:
UID: 1000
GID: 1000
uid=1000(dockrun) gid=1000(dockrun) groups=1000(dockrun)
As you can see, it uses the default values for group and user id (1000).
Running it with environmental variables set to something else, will produce a different output:
docker run -it -e PGID=934 -e PUID=937 --rm <docker-image-tag:version> id
Output:
User dockrun is running with the following IDs:
UID: 937
GID: 934
uid=937(dockrun) gid=934(dockrun) groups=934(dockrun)