Running a Docker container as a non root user
April 26, 2020
“Containerbow” by Michael Phillips Photography
The Problem: Docker writes files as root
Sometimes, when we run builds in Docker containers, the build creates files in a
folder that’s mounted into the container from the host (e.g. the source code
directory). This can cause us pain, because those files will be owned by the
root
user. When an ordinary user tries to clean those files up when preparing
for the next build (for example by using git clean
), they get an error and our
build fails.
There are a few ways we could deal with this problem:
-
We could try to prevent the build from creating any files, but that’s very limiting – we lose the ability to generate assets, or write any data to the disk. This is definitely too restrictive to solve the problem in a way that I could use with any build.
-
We could tell Git to ignore the affected files, but that carries the risk that they’ll hang around in the file system and have an effect on future builds. We’ve encountered that problem in the past at Redbubble, so we are wary about letting that happen again.
-
We could clean up the files at the end of the build, while we’re still running our Dockerised process. But that would require us to implement lots of error trapping logic to ensure the cleanup happens, but still exit the build with the correct result.
It would be more elegant if we could simply create files in a way that allows ordinary users to delete them. For example, we could tell Docker to run as an ordinary user instead of root.
Time to be someone else
Fortunately, docker run
gives us a way to do this: the --user
parameter.
We’re going to use it to specify the user ID (UID) and group ID (GID) that
Docker should use. This works because Docker containers all share the same
kernel, and therefore the same list of UIDs and GIDs, even if the associated
usernames are not known to the containers (more on that later).
To run our asset build, we could use a command something like this:
This will tell Docker to run its processes with user ID 1000 and group ID 1000. That will mean that any files created by that process also belong to the user with ID 1000.
docker container run --rm -it \
-v $(app):/app \ # Mount the source code
--workdir /app \ # Set the working dir
--user 1000:1000 \ # Run as the given user
my-docker/my-build-env:latest \ # Our build env image
make assets # ... and the command!
But I just want to be me!
But what if we don’t know the current user’s ID? Is there some way to automatically discover that?
There is!
id
is a program for finding out exactly this information. We can use it with
the -u
switch to get the UID, and the -g
switch to get the GID. So instead
of setting --user 1000:1000
, we could use subshells to set --user $(id
-u):$(id -g)
. That way, we can always use the current user’s UID and GID.
docker-compose
We often like to run our tests and things using docker-compose
, so that we can
spin up any required services as needed – databases and so on. So wouldn’t it be
nice if we could do this with docker-compose
as well?
Unfortunately, we can’t use subshells in a compose file – it’s not a
supported part of the format. Lucky for us, we can insert environment variables.
So if we have a docker-compose.yml
like this:
# This is an abbreviated example docker-compose.yml
version: '3.3'
services:
rspec:
image: my-docker/my-build-environment:latest
environment:
- RAILS_ENV=test
command: ["make", "assets"]
# THIS BIT!!!1!
user: ${CURRENT_UID}
volumes:
- .:/app
We could use a little bash to set that variable and start docker-compose
:
CURRENT_UID=$(id -u):$(id -g) docker-compose up
Et voila! Our Dockerised script will create files as if it were the host user!
Gotchas
Your user will be $HOME-less
What we’re actually doing here is asking our Docker container to do things using
the ID of a user it knows nothing about, and that creates some complications.
Namely, it means that the user is missing some of the things we’ve learned to
simply expect users to have – things like a home directory. This can be
troublesome, because it means that all the things that live in $HOME
–
temporary files, application settings, package caches – now have nowhere to
live. The containerised process just has no way to know where to put them.
This can impact us when we’re trying to do user-specific things. We found that
it caused problems using gem install
(though using Bundler is OK), or running
code that relies on ENV['HOME']
. So it may mean that you need to make some
adjustments if you do either of those things.
Your user will be nameless, too
It also turns out that we can’t easily share usernames between a Docker host and
its containers. That’s why we can’t just use docker run --user=$(whoami)
–
the container doesn’t know about your username. It can only find out about your
user by its UID.
That means that when you run whoami
inside your container, you’ll get a result
like I have no name!
. That’s entertaining, but if your code relies on knowing
your username, you might get some confusing results.
Wrapping up
We now have a way to use docker run
and docker-compose
to create files,
without having to use sudo
to clean them up!
Happy building!
I originally published this post on Medium. I think the ability to publish and keep control of one’s own work is one of the Internet’s most powerful benefits, so I’ve also posted it here, just on general principle.