dockerdocker-composedocker-volumedocker-multi-stage-build

Inconsistent behaviour with multi-stage build sharing volume between Docker Compose services?


Very inconsistent behaviour sharing the same volume and populating the folder in a Compose multi-stage build. Not all files are visible, only files from one container or another (depends on the run).

During the build, both write something in their public folder. The resulting volume only show files from the caddy stage or php stage, but not both.

In this run, public/build/ is missing:

$ docker compose exec caddy ls -la public
total 12
drwxr-xr-x    3 root     root          4096 May 18 17:47 .
drwxr-xr-x    1 root     root          4096 May 18 17:40 ..
drwxr-xr-x    3 root     root          4096 May 18 17:47 bundles

Same output for php service:

$ docker compose exec php ls -la public
total 12
drwxr-xr-x    3 root     root          4096 May 18 17:47 .
drwxrwxrwt    1 www-data www-data      4096 May 18 17:47 ..
drwxr-xr-x    3 root     root          4096 May 18 17:47 bundles

The docker file:

# [STAGE] php-prod
FROM php:fpm-alpine AS php
WORKDIR /var/www/html

# Create public/bundles/foo/bar.js
RUN set -eux; \
    mkdir -p public/bundles/foo; \
    touch public/bundles/foo/bar.js

# [STAGE] caddy
FROM caddy:latest as caddy
WORKDIR /srv

# Create public/build/baz.js
RUN set -eux; \
    mkdir -p public/build; \
    touch public/build/baz.js

And the Compose file:

version: "3.9"

services:
  caddy:
    build:
      context: ./
      target: caddy
    volumes:
      - public:/srv/public/

  php:
    build:
      context: ./
      target: php
    volumes:
      - public:/var/www/html/public/

volumes:
  public:

Can you explain the incosistent behaviour?


Solution

  • You can't use volumes to merge content from two different sources this way. There are a number of practical problems with using volumes to try to publish files out of one image to another container, and I'd avoid it entirely.

    The best approach here will be to COPY the files into the proxy image. With the setup you've shown, this is particularly straightforward since you're already building a custom image out of the same source tree.

    FROM php:fpm-alpine AS php
    ...
    
    FROM caddy:latest as caddy
    WORKDIR /srv
    
    # Create public/build/baz.js
    RUN mkdir -p public/build && \
        touch public/build/baz.js
    
    # Copy the files out of the PHP application image
    COPY --from=php /var/www/html/public/bundles/ ./public/bundles/
    

    Then when you run this, delete all of the volumes: blocks from the docker-compose.yml file.


    When you build an image, it does not create or populate volumes; the setup you have does not use the Dockerfile COPY lines to populate named volumes. (Try running docker-compose down -v, docker-compose build, and then docker volume ls; you will not have any volumes at this point.)

    The actual rules around named volumes are that, if

    1. The volume is a named or anonymous Docker volume, but not a bind mount or a mount in another container system like Kubernetes; and
    2. The volume is totally empty

    then, and only then, content is copied from the image at the mount point into the volume. Whatever's in the volume then hides what was in the image.

    This is consistent with the behavior you describe in the question. Whichever of the two containers starts first, its content gets copied into the volume; then when the second container is started, the volume isn't empty so nothing gets copied, but the volume content copied from the first image hides the second image's content.

    Other cases that won't work here: if you change the static files and rebuild the image, the volume won't get updated, so you won't see the change; if you use a bind mount instead of a named volume, you'll just get the empty directory from the host; if you go to deploy this on Kubernetes with a PersistentVolumeClaim, the volume will also remain empty.