DEV Community

Bruno Paz
Bruno Paz

Posted on • Updated on • Originally published at


Using Docker Multi-stage builds during development

Multi-stage builds is a feature introduced Docker 17.05 that allows you to create multiple intermediate images from the same Dockerfile.

With multi-stage builds, you can use multiple FROM statements in your Dockerfile. Each FROM instruction can use a different base, and each of them begins a new stage of the build. You can selectively copy artifacts from one stage to another, leaving behind everything you don’t want in the final image. You can read more about Multi-stage builds here.

This is very useful for example, to not include your application build dependencies in your final image, allowing you to have a much more smaller image.

Having a single binary in production image is great, but what about development? You will probably need your build dependencies to be present, and its recommended to have the same Dockerfile for both production and development.

For some time, It wasn't really clear how to do this, but its just one flag away.

The trick is to use "target" flag of the build command that allows you to specify which stage you want to stop your build.

For example:

Consider the following Dockerfile, which is responsible for building a Jekyll based static site.

FROM ruby:2.5.1-alpine3.7  AS build-env
RUN apk update && apk add --no-cache nodejs build-base
RUN apk add yarn --no-cache --repository --allow-untrusted
RUN mkdir -p /app
COPY Gemfile Gemfile.lock ./
RUN bundle install -j 4
COPY . ./
COPY package.json yarn.lock ./
RUN yarn install
RUN make _site


FROM nginx:1.13.0-alpine
WORKDIR /usr/share/nginx/html
COPY --from=build-env /app/_site ./

Enter fullscreen mode Exit fullscreen mode

As you can see, this Dockerfile has 2 "FROM" instructions. Each of them represents a stage in a Multi-stage build.

In the first stage, I install all the necessary tools to build a Jekyll application like ruby and bundler and yarn for frontend dependencies required by this specefic site.

The final result of this stage is a folder called "_site" with a bunch of HTML, Javascript and CSS. In production I just want to serve this static content, and I dont need all the ruby dependencies needed for building the site in the final image, so I create a new stage based on nginx-alpine image and I just copy the generated site contents from the build stage into it.

If I build the image now, the final image will just have the generated site.

This is great for production use, but during development I dont want to have to build the site everytime I do a change, and want to have nice things like "hot-reload" and "on the fly" assets compilation.

Thats where the "target" flag enters in action. This flag allows you to specify in which stage do you want your build to stop. so if you specify:

docker build . --target=build-env
Enter fullscreen mode Exit fullscreen mode

You will have an image exactly with the contents of that stage. With docker-compose its even simpler.

version: '3.4'
      context: .
      target: build-env
      - .:/app
      - '8082:80'
    command: 'env PORT=4001 HOST= yarn run dev'
Enter fullscreen mode Exit fullscreen mode

Note: target requires compose version > 3.4.

So when running

docker-compose up

in my dev environment I will end up with an image with all the development dependencies and the source code mounted as volume, where I can use live reload to immediately see my changes.

And thats it.

Understanding how Multi-stage builds works, opens your mind for a lot of possible use cases, like for example having a stage that installs your test dependencies to run unit tests, before the production build.

Top comments (1)

maniqui profile image
Julián Landerreche

It's not clear what the VOLUME /app directive on Dockerfile is helping with. What's the goal of "exposing" the /app folder from within the container to the host? At which step are you (re)using that volume?

Also, in your docker-compose file you have:

- .:/app

I understand that, for developing purposes (live reloading, etc), this directive will mount the project folder (from host) inside the container, "overwriting" the /app folder that you initially copied inside the image (for building purposes). Right?
What I'm missing is: how do you get all yarn dependencies installed when doing development tasks? Is "yarn run dev" enough to install them?

Yes, I see that you run "yarn install" during image build (for building/deploying purposes). But whatever that command install inside /app (assuming yarn stuff gets installed right there) is "overwritten" by the /app volume you are mounting for development purposes.

If you could clarify that (and maybe point to a Github repo that applies this workflow), that would be great.


The AI Brief

AI generated git commit messages

Minimize the struggle of remembering what you just coded. AI-generated commits make it easier to manage projects and keep track of changes. The Nutlope/aicommits project demonstrates how AI can improve commit messages.

I open sourced an AI that creates any UI in seconds

Make AI-generated user interfaces a breeze. This open-source project harnesses the power of generative AI technologies like chatGPT to create versatile, quick, and intuitive UI components.

Use AI to commit like a PRO in 1 second

Upgrade your commit message game with AI. Boost your productivity by using ChatGPT to generate commit messages and avoid context switching. OpenCommit is an open-source library that helps you achieve this easily.

Build your own ChatGPT starter kit

Train AI models on custom data for improved domain-specific knowledge. Combine the power of WebView technologies and this starter kit to train your ChatGPT model on specific websites, allowing for better-optimized outcomes.