aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/_posts/2020-05-06-docker-bind-mounts.md
diff options
context:
space:
mode:
Diffstat (limited to '_posts/2020-05-06-docker-bind-mounts.md')
-rw-r--r--_posts/2020-05-06-docker-bind-mounts.md176
1 files changed, 0 insertions, 176 deletions
diff --git a/_posts/2020-05-06-docker-bind-mounts.md b/_posts/2020-05-06-docker-bind-mounts.md
deleted file mode 100644
index 9853331..0000000
--- a/_posts/2020-05-06-docker-bind-mounts.md
+++ /dev/null
@@ -1,176 +0,0 @@
----
-title: 'Docker: bind mounts & file ownership'
-excerpt: Docker + bind mounts + non-root users = pain.
----
-If you want to:
-
-1. run your Docker service as a user other than root,
-2. share a writable directory between your host and the container,
-
-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:
-
- docker run -it --rm -v "$( pwd ):/data" alpine touch /data/test.txt
-
-would create file ./test.txt owned by root:root.
-
-You can fix that by using the `--user` parameter:
-
- docker run -it --rm -v "$( pwd ):/data" --user "$( id -u ):$( id -g )" alpine touch /data/test.txt
-
-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][Redis Dockerfile] and
-[MongoDB][MongoDB Dockerfile].
-
-[Redis Dockerfile]: https://github.com/docker-library/redis/blob/cc1b618d51eb5f6bf6e3a03c7842317b38dbd7f9/6.0/Dockerfile#L4
-[MongoDB Dockerfile]: https://github.com/docker-library/mongo/blob/5cbf7be9a486932b7e472a39e432c9a444628465/4.2/Dockerfile#L4
-
-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`:
-
- $ cat Dockerfile
- 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
-
- $ docker build -t id .
- ...
-
- $ docker run -it --rm id
- uid=9099(test-user) gid=9099(test-group)
- root root
- test-user test-group
-
- $ docker run -it --rm --user root id
- 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][Redis entrypoint],
-[MongoDB][MongoDB entrypoint]).
-
-[Redis entrypoint]: https://github.com/docker-library/redis/blob/cc1b618d51eb5f6bf6e3a03c7842317b38dbd7f9/6.0/docker-entrypoint.sh#L11
-[MongoDB entrypoint]: https://github.com/docker-library/mongo/blob/5cbf7be9a486932b7e472a39e432c9a444628465/4.2/docker-entrypoint.sh#L12
-
-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):
-
- $ mkdir data
-
- $ stat -c '%u' data
- 1000
-
- $ docker run -it --rm --name redis -v "$( pwd )/data:/data" redis:6.0
- ...
-
- $ stat -c '%u' data
- 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 accomodates for it by _not_ changing its owner if the container is
-run as anybody other than root.
-For example:
-
- $ mkdir data
-
- $ stat -c '%u' data
- 1000
-
- $ docker run -it --rm --name redis -v "$( pwd )/data:/data" --user "$( id -u ):$( id -g )" redis:6.0
- ...
-
- $ stat -c '%u' data
- 1000
-
-Sometimes `--user` is not enough though.
-That specified user is almost certainly missing from container's /etc/passwd,
-it doesn't have a $HOME, etc.
-All of that could cause problems with some applications.
-
-One scenario I had to deal with is making an image that bundles all the gems
-(and a specific Ruby version) for my Ruby web application.
-That application shouldn't be run as root, but it must be able to pick up code
-changes on the fly, and I should be able `docker exec` into the container,
-update the dependencies (along with Gemfile[.lock], and those changes should be
-reflected on the host without messing up file metadata), and restart the app.
-It's quite easy to install the dependencies in the Dockerfile, but they (along
-with the mapped Gemfile[.lock]) should be writable by the user running the
-service.
-The solution often suggested is to create a container user with a fixed UID
-(that would match the host user UID).
-That way, it would be able to update the dependencies stored in the container,
-as well as write to the bind mount owned by the host user with the same UID.
-Additionally, file ownership info would be preserved on the host!
-
-We can create a user with a fixed UID when
-
-1. building the image (using build `ARG`uments),
-2. first starting the container by passing the required UID using environment
-variables.
-
-The advantages of creating the user when building the image is that we can also
-install the dependencies in the Dockerfile, thus eliminating the need to
-rebuild them for every other application.
-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 has the advantage of not
-requiring image rebuilds.
-But, as the dependencies need to be installed after creating the user, you'd
-have to waste resources by installing them for every user and every app on
-every machine (each time when creating 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).
-
-[jekyll-docker]: https://github.com/egor-tensin/jekyll-docker
-
-Useful links
-------------
-
-* [Docker and \-\-userns-remap, how to manage volume permissions to share data between host and container?](https://stackoverflow.com/q/35291520/514684)
-* [What is the (best) way to manage permissions for Docker shared volumes?](https://stackoverflow.com/q/23544282/514684)
-* [Handling Permissions with Docker Volumes](https://denibertovic.com/posts/handling-permissions-with-docker-volumes/)
-* [File Permissions: the painful side of Docker](https://blog.gougousis.net/file-permissions-the-painful-side-of-docker/)
-* [Avoiding Permission Issues With Docker-Created Files](https://vsupalov.com/docker-shared-permissions/)