
How to reduce your Docker image size with Docker Multi Stage builds
There are many great benefits to using Docker multi stage builds such as reducing the attack surface and faster deployments. These warrant their own articles but today I will be discussing how to reduce the size of a Docker image by using Docker Multi stage builds.
What are Multi Stages
Docker Multi Stage builds allow for a Docker file to be split into separate stages. Each stage can be treated like it's own separate Docker image which installs only what is needed for that stage to be built. A stage can be extended from a pervious stage and the contents from one stage can copied to a preceding stage.
A traditional Docker image will need to have a web server and php installed with all the required composer and npm dependencies. The webserver will also need to be configured and site files imported into the image. Instead of doing all of this in a single Docker image we could seperate each of the stages. These build stages could be defined like so:
- Stage 1: Create the base layer
- Stage 2: Install the composer packages
- Stage 3: Install the frontent packages
- Stage 4: Create the final image with copied files
Let's create each of these stages
Stage 1: The base layer
In this stage we are only concerned with the webserver, the php version and its extensions.
FROM php:7.4-apache-buster as webserver-base
LABEL maintainer="Peter Fisher"
ARG DEBIAN_FRONTEND=noninteractive
ENV APACHE_DOCUMENT_ROOT="/var/www/html/public"
RUN apt-get update --fix-missing \
&& apt-get install -y --no-install-recommends mariadb-client zlib1g-dev libcurl3-dev libssl-dev \
&& docker-php-ext-install pdo pdo_mysql iconv \
&& a2enmod rewrite \
&& a2enmod headers
Stage 2: The composer packages
In this stage we are only concerned with the composer packages. We only need the composer.json
and composer.lock files
. At this point the packages will only be installed into this stage.
FROM composer as pm-backend
COPY composer.json composer.json
COPY composer.lock composer.lock
RUN composer install \
--ignore-platform-reqs \
--no-interaction \
--no-plugins \
--no-scripts \
--prefer-dist
Stage 3: The frontend packages
Like with stage 2 we are not concerned with how the webserver is built. At this stage we are only concerned with the required dependancies of the frontend. This means we can install the frontend packages in isolation.
FROM node as pm-frontend
RUN mkdir -p /app/public
COPY package.json /app/
WORKDIR /app
RUN npm install
Stage 4: Putting it all together
At this point we have built the webserver, the composer packages and frontend dependencies in their own stages. The first stage that we created will be used as the base of the final image and this can be done by extending the webserver-base
.
The build artifacts from these stages such as the vendor and node_modules folder need to be copied into the final image. We do this by extending the base layer and copying the built files into the final image.
FROM webserver-base as webserver
COPY . /var/www/html
COPY --from=pm-backend /app/vendor/ /var/www/html/vendor/
COPY --from=pm-frontend /app/public/js/ /var/www/html/public/js/
COPY --from=pm-frontend /app/public/css/ /var/www/html/public/css/
These are all of the stages in a Docker file
# Stage 1: The base layer
FROM php:7.4-apache-buster as webserver-base
LABEL maintainer="Peter Fisher"
ARG DEBIAN_FRONTEND=noninteractive
ENV APACHE_DOCUMENT_ROOT="/var/www/html/public"
RUN apt-get update --fix-missing \
&& apt-get install -y --no-install-recommends mariadb-client zlib1g-dev libcurl3-dev libssl-dev \
&& docker-php-ext-install pdo pdo_mysql iconv \
&& a2enmod rewrite \
&& a2enmod headers
# Stage 2: The composer packages
FROM composer as pm-backend
COPY composer.json composer.json
COPY composer.lock composer.lock
RUN composer install \
--ignore-platform-reqs \
--no-interaction \
--no-plugins \
--no-scripts \
--prefer-dist
# Stage 3: The frontend packages
FROM node as pm-frontend
RUN mkdir -p /app/public
COPY package.json /app/
WORKDIR /app
RUN npm install
# Stage 4: The final Docker image
FROM webserver-base as webserver
COPY . /var/www/html
COPY --from=pm-backend /app/vendor/ /var/www/html/vendor/
COPY --from=pm-frontend /app/public/js/ /var/www/html/public/js/
COPY --from=pm-frontend /app/public/css/ /var/www/html/public/css/
Advantages of Docker multi stage builds
- Build artefacts such as the vendor and node_modules can be copied from one stage to another
- Stages can be extended from other stages
- A consistent build pipeline can be created
- The final image size is far smaller as the previous stages are not combined with the final image.
- The attack surface is reduced as the build tools are not included in the final image
Disadvantages of Docker multi stage builds
- Multi stage builds can create complex Docker files.
- More post build scripts may be required to update dependencies
- A change to one stage may have unexpected side effects to a future stage
- You cannot run the last stage before the first stage