Docker Compose - services unable to share a named volume when built with context

Viewed 151

My goal is to build two containers using docker-compose. Both containers will read/write to a shared volume.

For simplicity lets say Dockerfile.a looks like:

FROM busybox:latest
WORKDIR /app
RUN touch /app/apple.txt

Dockerfile.b:

FROM busybox:latest
WORKDIR /app
RUN touch /app/banana.txt

and docker-compose.yml

version: "3.8"

services:
  a:
    image: sandbox/a:v1.0
    volumes:
      - setupapp-vol:/app
    build:
      context: .
      dockerfile: Dockerfile.a

  b:
    image: sandbox/b:v1.0
    volumes:
      - setupapp-vol:/app
    build:
      context: .
      dockerfile: Dockerfile.b

volumes:
  setupapp-vol:

Run docker-compose up and check what's in each container.

# ls
banana.txt
# exit
~/sandbox$ docker run -v setupapp-vol:/app -it sandbox/a:v1.0 sh
# ls
banana.txt
# exit

My question is: Why don't I find both apple.txt and banana.txt?

If I don't use a separate build context, the following docker-compose.yml succeeds in both services writing to the shared volume. (But I really need a separate build context because the real Dockerfiles are more complicated than what I've described.)

version: "3.8"
services:
  a:
    image: busybox:latest
    volumes:
      - setupapp:/app
    command: "touch /app/apple.txt"
  b:
    image: busybox:latest
    volumes:
      - setupapp:/app
    command: "touch /app/banana.txt"  
volumes:
  setupapp:
    driver:local
1 Answers

There's a lot going on here. If you can restructure your broader application to not need file sharing between containers, it will be more robust.

In your first example, you have two separate images. Image a contains the file /app/apple.txt and b contains the file /app/banana.txt. When you launch containers on both of them, you mount the named volume setupapp-vol over the /app directory.

The first time only a container gets launched with an empty named volume, the contents from its image get copied into the volume. If there is already content in the volume, it hides what was in the image. This means that if image b gets updated, those updates will get lost, and if image a contains different content, there's no way to see it. A sequence consistent with your observation is:

  1. Container b gets started.
  2. The named volume is empty, so /app/banana.txt gets duplicated from image b into the volume.
  3. Container a gets started.
  4. The named volume is not empty, so its content hides the contents of the volume.

You should be able to further demonstrate this by manipulating the volume content:

# manipulates the contents of the shared volume
docker-compose run b \
  mv /app/banana.txt /app/coconut.txt
# will show only coconut, not apple or banana
docker-compose run a \
  ls /app

Similarly, if you change the filename in Dockerfile.b and re-run docker-compose up --build, you won't see that change in the volume contents: there is already content in the volume and it hides what's in the image.

(This is the same behavior behind many SO questions around changes in a Node application's package.json being ignored: the node_modules directory is kept in a volume and the old volume contents override the updated image.)


In your second example, you are overriding the main container command: to create the files. There's no content in the image, and it is a plain busybox image. In your original Dockerfiles, this would be similar to changing RUN to CMD for the touch command. Now that sequence is:

  1. Container b gets started, mounting the volume. There's no /app directory in the image, so there's no auto-duplication behavior.
  2. The main container command creates /app/banana.txt in the volume and the container exits.
  3. Container a gets started, mounting the volume.
  4. The main container command creates /app/apple.txt in the volume and the container exits.

That is, the difference here is that first the volume contents get mounted into the container (hiding the image contents) and then the main container command runs. In your first example the file is in the image (and gets hidden) but in the second it is created by the main image command (after the mount).


So say you have two images, and they each have some data, and you can't get around trying to merge the data into a single volume. From the examples above we have to do that after the containers start (and the volume gets mounted).

It's a little bit awkward, but you can do this with an entrypoint wrapper script that runs at container startup time. That copies the data from somewhere else in the image, then runs whatever it was given as the main container command:

#!/bin/sh
cp -a /data/* /app
exec "$@"

In your image, COPY the files into this template directory, make the script above be the ENTRYPOINT, and leave the CMD as it was before. (If you had split the command into two words, combine them into CMD; for example CMD ["python3", "app.py"].)

FROM busybox:latest
RUN touch /data/apple.txt
COPY entrypoint.sh /usr/local/bin
WORKDIR /app
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
# CMD ...

Now the data will be built into the image (as in the first example) but when you start it up it will copy its own data into the volume (as in the second example).

Related