How can I have a host and container read/write the same files with Docker?

There are multiple ways to do this, but the central issue is that bind mounts do not include any UID mapping capability, the UID on the host is what appears inside the container and vice versa. If those two UID’s do not match, you will read/write files with different UID’s and likely experience permission issues.


Option 1: get a Mac or deploy docker inside of VirtualBox. Both of these environments have a filesystem integration that dynamically updates the UID’s. For Mac, that is implemented with OSXFS. Be aware that this convenience comes with a performance penalty.


Option 2: Change your host. If the UID on the host matches the UID inside the container, you won’t experience any issues. You’d just run a usermod on your user on the host to change your UID there, and things will happen to work, at least until you run a different image with a different UID inside the container.


Option 3: Change your image. Some will modify the image to a static UID that matches their environment, often to match a UID in production. Others will pass a build arg with something like --build-arg UID=$(id -u) as part of the build command, and then the Dockerfile with something like:

FROM alpine
ARG UID=1000
RUN adduser -u ${UID} app

The downside of this is each developer may need a different image, so they are either building locally on each workstation, or you centrally build multiple images, one for each UID that exists among your developers. Neither of these are ideal.


Option 4: Change the container UID. This can be done in the compose file, or on a one off container with something like docker run -u $(id -u) your_image. The container will now be running with the new UID, and files in the volume will be accessible. However, the username inside the container will not necessarily map to your UID which may look strange to any commands you run inside the container. More importantly, any files own by the user inside the container that you have not hidden with your volume will have the original UID and may not be accessible.


Option 5: Give up, run everything as root, or change permissions to 777 allowing everyone to access the directory with no restrictions. This won’t map to how you should run things in production, and the container may still write new files with limited permissions making them inaccessible to you outside the container. This also creates security risks of running code as root or leaving filesystems open to both read and write from any user on the host.


Option 6: Setup an entrypoint that dynamically updates your container. Despite not wanting to change your image, this is my preferred solution for completeness. Your container does need to start as root, but only in development, and the app will still be run as the user, matching the production environment. However, the first step of that entrypoint will be to change the user’s UID/GID inside the container to match your volume’s UID/GID. This is similar to option 4, but now files inside the image that were not replaced by the volume have the right UID’s, and the user inside the container will now show with the changed UID so commands like ls show the username inside the container, not a UID to may map to another user or no one at all. While this is a change to your image, the code only runs in development, and only as a brief entrypoint to setup the container for that developer, after which the process inside the container will look identical to that in a production environment.

To implement this I make the following changes. First the Dockerfile now includes a fix-perms script and gosu from a base image I’ve pushed to the hub (this is a Java example, but the changes are portable to other environments):

FROM openjdk:jdk as build
# add this copy to include fix-perms and gosu or install them directly
COPY --from=sudobmitch/base:scratch / /
RUN  apt-get update \
 &&  apt-get install -y maven \
 &&  useradd -m app
COPY code /code
RUN  mvn build
# add an entrypoint to call fix-perms
COPY entrypoint.sh /usr/bin/
ENTRYPOINT ["/usr/bin/entrypoint.sh"]
CMD ["java", "-jar", "/code/app.jar"]
USER app

The entrypoint.sh script calls fix-perms and then exec and gosu to drop from root to the app user:

#!/bin/sh
if [ "$(id -u)" = "0" ]; then
  # running on a developer laptop as root
  fix-perms -r -u app -g app /code
  exec gosu app "$@"
else
  # running in production as a user
  exec "$@"
fi

The developer compose file mounts the volume and starts as root:

version: '3.7'
volumes:
  m2:
services:
  app:
    build:
      context: .
      target: build
    image: registry:5000/app/app:dev
    command: "/bin/sh -c 'mvn build && java -jar /code/app.jar'"
    user: "0:0"
    volumes:
    - m2:/home/app/.m2
    - ./code:/code

This example is taken from my presentation available here: https://sudo-bmitch.github.io/presentations/dc2019/tips-and-tricks-of-the-captains.html#fix-perms

Code for fix-perms and other examples are available in my base image repo: https://github.com/sudo-bmitch/docker-base

Leave a Comment