TIL: User and group management under BusyBox

Note: This post is part of my TIL (“today I learned”) series, where I describe (usually) short problems and their solutions I encountered that day. Find all posts of this series here.

TLDR

BusyBox’s version of adduser and addgroup can be used to add users to groups (usually done via usermod).

# this common usermod invocation to add $GROUP to $USERs groups
usermod -aG $GROUP $USER
# is equivalent to the following BusyBox adduser call
adduser $USER $GROUP # note the swapped order of $GROUP and $USER
# or even this 

How I got there

In a recent effort to build a custom nginx docker image that includes the awesome ngx_http_geoip2 module, I took a closer look at the BusyBox man pages and more specifically its adduser and addgroup “applets” (as they call their commands).

A common task my custom docker images solve for me, is the creation of specific user and/or groups that either differ from or straight up don’t exist in the upstream images. In the case of the nginx image, I wanted run it under a www-data user that matches my host system (compared to the nginx user and group with UID/GID 101 that the image uses). In past images, I had often used usermod which is available on Alpine via the shadow package to add existing users to a newly created group. However, I had always wondered if one really needed to install a package to “just modify a user’s groups” after initial creation.

For some reason, the addition of a new image to my collection (which is a rare task compared to the regular version bumps for new Alpine releases), prompted me to investigate if shadow was really needed. I cannot quite recall, where I got the initial solution from 1, but among BusyBox’ many commands there was bound to be one offering this functionality. And sure enough, addgroup and adduser (among the first three commands(!)) both offer a way to emulate the desired behavior, as the following snipped shows:

# this common usermod invocation to add $GROUP to $USERs groups
usermod -aG $GROUP $USER
# is equivalent to the following BusyBox adduser call
adduser $USER $GROUP # note the swapped order of $GROUP and $USER
# or even this 

While I was both amazed and bummed out2 by my discovery at the same time, I quickly started to update the latest tags of my existing images producing the following, embarassingly short diff (see also the full commit on GitHub):

diff --git a/rspamd/alpine-3.21/Dockerfile b/rspamd/alpine-3.21/Dockerfile
index b0f46be..343616b 100644
--- a/rspamd/alpine-3.21/Dockerfile
+++ b/rspamd/alpine-3.21/Dockerfile
@@ -5,15 +5,15 @@ EXPOSE 11333 11334

 ARG VMAIL_GID="5000"

-RUN apk add --no-cache shadow \
+RUN apk add --no-cache \
     rspamd \
     rspamd-controller \
     rspamd-proxy \
     ca-certificates \
     && rm -rf /var/cache/apk/*

-RUN addgroup --gid ${VMAIL_GID} vmail \
-    && usermod -aG vmail rspamd
+RUN addgroup -g ${VMAIL_GID} vmail \
+    && adduser rspamd vmail

 RUN mkdir /run/rspamd \
     && chown rspamd:vmail /run/rspamd \

It’s simple, yet beautiful: I opted for the addgroup variant in this image to match the previous usage and the diff could have been even shorter, had I not also shortened the --gid argument in the same commit.

Naturally, I wanted to know how big the impact of this simple change would be on the sizes of my images3. Using dive (big shoutout, its visualization of image layers, helped me immensely when initially familiarizing myself with containers), I saw a decrease from 80mb to 78mb for the rspamd container also shown in the diff above. Not bad for a simple (almost) one-line change with no downsides!


1

Probably an instance of the XY problem of me searching for usermod under Alpine/BusyBox rather than looking for a solution of how to add a user to a group

2

For not having invested the 15 minutes to find the proper solution years ago

3

Which, as we all know, is the most important metric for any docker image..