2020 05 06
If you want to:
you’re in for a treat! The thing is, files stored in the shared directory retain their ownership (and by that I mean their UIDs and GIDs, as they’re the only thing that matters) after being mounted in the container.
Case in point:
would create file ./test.txt owned by root:root.
You can fix that by using the --user
parameter:
That would create file ./test.txt owned by the current user (if the current working directory is writable by the current user, of course).
More often though, instead of a simple touch
call, you have a 24/7 service,
which absolutely mustn’t run as root, regardless of whether --user
was
specified or not.
In such cases, the logical solution would be to create a regular user in the
container, and use it to run the service.
In fact, that’s what many popular images do, i.e. Redis and
MongoDB.
How do you run the service as regular user though?
It’s tempting to use the USER
directive in the Dockerfile, but that can be
overridden by --user
:
FROM alpine RUN addgroup --gid 9099 test-group && \ adduser \ --disabled-password \ --gecos '' \ --home /home/test-user \ --ingroup test-group \ --uid 9099 \ test-user RUN touch /root.txt USER test-user:test-group RUN touch /home/test-user/test-user.txt CMD id && stat -c '%U %G' /root.txt && stat -c '%U %G' /home/test-user/test-user.txt
uid=9099(test-user) gid=9099(test-group) root root test-user test-group
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video) root root test-user test-group
I suppose that’s the reason why many popular images override ENTRYPOINT, using
a custom script (and gosu
, which is basically sudo
, I think) to forcefully
drop privileges (for example, see Redis,
MongoDB).
Now, what if such service needs persistent storage? A good solution would be to use Docker volumes. For development though, you often need to just share a directory between your host and the container, and it has to be writable by both the host and the container process. This can be accomplished using bind mounts. For example, let’s try to map ./data to /data inside a Redis container (this assumes ./data doesn’t exist and you’re running as regular user with UID 1000; press Ctrl+C to stop Redis):
1000
999
As you can see, ./data changed its owner from user with UID 1000 (the host
user) to user with UID 999 (the redis
user inside the container).
This is done in Redis’ ENTRYPOINT script, just before dropping root privileges
so that the redis-server
process owns the /data directory and thus can write
to it.
If you want to preserve ./data ownership, Redis’ image (and many others) explicitly accommodates for it by not changing its owner if the container is run as anybody other than root. For example:
1000
1000
Sometimes --user
is not enough though.
The specified user is almost certainly missing from container’s /etc/passwd, it
doesn’t have a $HOME directory, etc.
All of that could cause problems with some applications.
The solution often suggested is to create a container user with a fixed UID (that would match the host user UID). That way, the app won’t be run as root, the user will have a proper entry in /etc/passwd, it will be able to write to the bind mount owned by the host user, and it won’t have to change the directory’s permissions.
We can create a user with a fixed UID when
ARG
uments),The advantage of creating the user when building the image is that we can also do additional work in the Dockerfile (like if you need to install dependencies as that user). The disadvantage is that the image would need to be rebuilt for every user on every machine.
Creating the user when first starting the container switches the pros and cons. You don’t need to rebuild the image every time, but you’ll have to waste time and resources by doing the additional work that could’ve been done in the Dockerfile every time you create a container.
For my project jekyll-docker I opted for the former approach, making sure the
jekyll
process runs with the same UID as the user who built the image (unless
it was built by root, in which case it falls back to a custom UID of 999).
Seems to work quite nicely in practice.
My blog. Feel free to contribute or contact me.