A use case for multi-stage builds

Lukasz
3 min readSep 25, 2023
Photo by Library of Congress on Unsplash

Upon revisiting one of my past projects, I attempted to build the docker image I once prepared but encountered a failure:

$ docker build -t fortune-api:0.4.2 \
'https://github.com/lbacik/fortune-api.git#v0.4.2'
...
6.520 Reading state information...
6.523 E: Unable to locate package fortunes-off
6.523 E: Unable to locate package fortunes-fr
...
ERROR: failed to solve:
process "/bin/sh -c apt-get -y update && apt-get -y install ..."
did not complete successfully: exit code: 100

It had definitely worked in the past – the previously created image is still functional: https://fortune-api.luka.sh/.

Why does it fail now? 🤔

A glimpse at Dockerfile (v0.4.2):

FROM python:3.9

ENV FORTUNES=/usr/share/games/fortunes
ENV CORS=yes

RUN apt-get -y update && apt-get -y install \
fortunes \
fortunes-off \
fortune-anarchism \
fortunes-bg \
fortunes-bofh-excuses \
fortunes-br \
fortunes-cs \
fortunes-de \
fortunes-debian-hints \
fortunes-es \
fortunes-es-off \
fortunes-fr \
fortunes-ga \
fortunes-it \
fortunes-it-off \
fortunes-mario \
fortunes-pl \
fortunes-ru \
fortunes-zh

COPY . /opt/project
WORKDIR /opt/project

RUN pip install -r requirements.txt

EXPOSE 8080

CMD uwsgi --ini config.ini

The problem is the Debian distro version, which the python:3.9 image has been built on. Dockerhub shows now:

When I created the Dockerfile and built it previously, the python:3.9 image used Debian’s “Bullseye” version (debian:11).

The latest version of Debian was released on June 10, 2023. Unfortunately, it caused my Dockerfile build to fail due to package changes.

Certainly, there is a straightforward solution to my problem. I can specify the image version more precisely as python:3.9-bullseye. This should work, but it doesn’t seem too elegant. My application requires data — but the source of this data is not essential (for the app) and can change!

Take a look:

FROM debian:11 as base

# hadolint ignore=DL3008,DL3015
RUN apt-get -y update && apt-get -y install \
fortunes \
fortunes-off \
fortune-anarchism \
fortunes-bg \
fortunes-bofh-excuses \
fortunes-br \
fortunes-cs \
fortunes-de \
fortunes-debian-hints \
fortunes-es \
fortunes-es-off \
fortunes-fr \
fortunes-ga \
fortunes-it \
fortunes-it-off \
fortunes-mario \
fortunes-pl \
fortunes-ru \
fortunes-zh

FROM python:3.9

ENV FORTUNES=/usr/share/games/fortunes
ENV CORS=yes

COPY --from=base /usr/share/games/fortunes /usr/share/games/fortunes

COPY . /opt/project
WORKDIR /opt/project

# hadolint ignore=DL3042
RUN pip install -r requirements.txt

EXPOSE 8080

CMD ["uwsgi", "--ini", "config.ini"]

A multi-stage build fits here great, doesn’t it?!

And for Debian 12:

FROM debian:12 as base

# hadolint ignore=DL3008,DL3015
RUN apt-get -y update && apt-get -y install \
fortune-anarchism \
fortune-mod \
fortunes-bg \
fortunes-bofh-excuses \
fortunes-br \
fortunes-cs \
fortunes-de \
fortunes-debian-hints \
fortunes-eo \
fortunes-es-off \
fortunes-es \
fortunes-ga \
fortunes-it-off \
fortunes-it \
fortunes-mario \
fortunes-min \
fortunes-pl \
fortunes-ru \
fortunes-zh \
fortunes

FROM python:3.9

ENV FORTUNES=/usr/share/games/fortunes
ENV CORS=yes

COPY --from=base /usr/share/games/fortunes /usr/share/games/fortunes

COPY . /opt/project
WORKDIR /opt/project

# hadolint ignore=DL3042
RUN pip install -r requirements.txt

EXPOSE 8080

CMD ["uwsgi", "--ini", "config.ini"]
  1. The app part doesn’t change
  2. There is a separate step to prepare the required data (this step can be easily aligned to the appropriate source)

Isn’t it much cleaner?

--

--