How can I keep all artifacts when building multi-stage Dockerfiles with docker-compose?

Viewed 531

I'm working on a project that uses a multi-stage Dockerfile. This works fine for CircleCI, but I'm am not very experienced with Docker so I'm having trouble running things locally. From the documentation:

The second FROM instruction starts a new build stage with the alpine:latest image as its base. The COPY --from=0 line copies just the built artifact from the previous stage into this new stage. The Go SDK and any intermediate artifacts are left behind, and not saved in the final image.

If I understand this correctly, it means that if I install a set of dependencies in stage 1, and another set in stage 2, then after I run docker-compose up I will only see some of dependencies installed. This is what is happening locally. However, in order to attach to a container and run the development server, I need all project dependencies to be installed. I have read here that it is possible to use the --rm=false parameter to the build command (I'm not sure if this refers to docker build, docker-compose build, or if there is any difference). I tried to add a rm setting to my docker-compose but apparently it doesn't exist.

version: "3.8"
services:
    app:
        container_name: my.container
        build:
            context: .
            dockerfile: Dockerfile.dev
            rm: false
        entrypoint: sh -c
        command: tail -f /dev/null
        stdin_open: true
        tty: true
    app2:
        ...

My Dockerfile looks roughly like this:

ARG PYTHON_VERSION=3.8-alpine3.12

FROM python:$PYTHON_VERSION AS base
ARG USER=myuser

# Install application run-time dependencies.
RUN addgroup -S ${USER} && \
    adduser -S -g ${USER} ${USER} && \
    apk add --no-cache \
            --upgrade \
        bash && \
    rm -rf /var/cache/apk/*

FROM base AS builder

RUN mkdir /install
RUN mkdir /e2e_install
WORKDIR /install

# Install application build-time dependencies.
RUN apk update && \
    apk add --no-cache --upgrade \
    bash \
    build-base \
    libffi-dev \
    pcre-dev \
    linux-headers \
    zlib-dev \
    python3-dev \
    openssh \
    openssh-keygen \
    git \
    gcc \
    musl-dev && \
    rm -rf /var/cache/apk/*

# Install project and test dependencies.
RUN pip install --no-cache-dir --disable-pip-version-check pip==20.0.2

COPY requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir \
    --no-warn-script-location \
    --prefix=/install \
    --disable-pip-version-check -r /tmp/requirements.txt

COPY test_requirements.txt /tmp/test_requirements.txt
RUN pip install --no-cache-dir \
    --ignore-installed \
    --no-warn-script-location \
    --prefix=/install \
    --disable-pip-version-check -r /tmp/test_requirements.txt

# install e2e-test dependencies
COPY e2e/requirements.txt /tmp/e2e_test_requirements.txt
RUN cat /tmp/e2e_test_requirements.txt
RUN pip install --no-cache-dir \
                --ignore-installed \
                --disable-pip-version-check \
                --no-warn-script-location \
                --prefix=/e2e_install \
                -r /tmp/e2e_test_requirements.txt

# Tag version number into source code.
ARG VERSION_NUMBER=unversioned
WORKDIR /source
COPY . .
RUN sed -i -e "s/__version__ = \"unversioned\"/__version__ = \"${VERSION_NUMBER}\"/" external_api/__init__.py

# -------------------- e2e Image --------------------
FROM base as e2e-test
ARG USER=myuser

COPY --from=builder /e2e_install /usr/local

WORKDIR /opt/company/myapp
COPY --from=builder /source/e2e ./e2e
COPY --from=builder /source/scripts/entry_point.sh .

# Set file owner group,
RUN chown -R ${USER}:${USER} /opt/company
ENTRYPOINT ["./entry_point.sh"]

# -------------------- Production Image --------------------
FROM base
ARG USER=myuser

# Copy necessary files from builder.
WORKDIR /opt/company
COPY --from=builder /install /usr/local
COPY --from=builder /source .

# Set file owner group,
RUN chown -R ${USER}:${USER} /opt/company

# Run unit tests.
RUN PYTHONPATH=. pytest tests/unit_test --cov=. --cov-report=xml

COPY ./scripts/entry_point.sh .
ENTRYPOINT ["./entry_point.sh"]

When I run docker-compose build, I get a bunch of these:

Step 20/26 : ARG VERSION_NUMBER=unversioned
 ---> Running in 1ce3e682b483
Removing intermediate container 1ce3e682b483
 ---> dcc4d4b6b4b5
Step 21/26 : WORKDIR /source
 ---> Running in c8d9c1349305
Removing intermediate container c8d9c1349305

When I attach to the container and try to run the Django development server, it doesn't work because the dependencies are not installed (even though the docker-compose build output shows that they were). I suppose this is because "intermediate containers" are being removed. I have tried to transform the multi-stage Dockerfile into a single-stage one, but I still keep getting "Removing intermediate container" messages and the dependencies are not all installed. Do I have to install all dependencies in a single RUN instruction? Is this the only way to have the dependencies all installed when I enter the container? Is there no way for me to keep this Dockerfile intact and somehow tell the docker-compose to stop "leaving intermediate artifacts behind" when building from it? Am I even using the word "artifact" correctly? I can't find a clear explanation and I'm not sure I fully understand what a "docker artifact" is.

1 Answers

It looks to me like you need to have separate docker-compose services, one for each of your images. You say there are 4 separate Python projects involved, so it sounds like there should be (at least) 4 services defined in your docker-compose.yaml.

So rather than defining all your services in one Dockerfile, I would expect each service to have its own Dockerfile, and to be defined separately in your docker-compose.yaml.

version: "3.7"

services:

  e2e:
    - build ./e2e
    ...
  production:
    - build ./production
    ...

You say all your services have to communicate with one another. Docker Compose makes this very easy as, for example, the production image can access the e2e image just using the URI e2e. (e.g. curl e2e).

Related