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).
public
caddy
mounts public
at /srv/public/
php
mounts public
at /var/www/html/public/
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?
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
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.