DEV Community

Joseph D. Marhee
Joseph D. Marhee

Posted on

Using Multi-stage Dockerfiles

Docker supports the capability to use the Dockerfile declaration to perform an image build in multiple steps called a multi-stage build.

A while ago, I built a Ruby application that provisions and deploys a Docker-based VPN server on DigitalOcean that, itself, runs in a Docker container. Before it ran on Kubernetes, however, I ran it on pre-imaged hosts that had dependencies baked into it, which make spinning up new instances very quick.

I had the same capability in Docker, to build a base image in an earlier stage (incidentally, using the same CI/CD tooling I used for my base instance images), so when I went to deploy it, I would have those available and not have to perform the entire build at deploy-time. However, initially, I didn't know this, and had a Dockerfile that looked like this:

FROM ruby:2.2.4

ADD app.rb /app/app.rb
ADD Gemfile /app/Gemfile
ADD views/index.erb /app/views/index.erb
ADD views/confirmation.erb /app/views/confirmation.erb
ADD environment.rb /app/environment.rb
WORKDIR /app
RUN bundle install

ENTRYPOINT ["ruby","app.rb","-o","0.0.0.0"]

It got the job done, but took a long time each time I deployed the rebuilt image because of the added steps further down my pipeline each time the image was touched.

So, I created a base image that only handled installing the Gem dependencies first, as its own step:

FROM ruby:2.2.4 as rb-base

ADD Gemfile /root/Gemfile
WORKDIR /root
RUN bundle install

I will reference resources from this image through the alias I gave it above, rb-base, and move on to the image I will end up pushing, so I'll append the following to the Dockerfile:

FROM ruby:2.2.4 as rb-app
MAINTAINER Joseph D. Marhee <joseph@marhee.me>
WORKDIR /app
COPY --from=rb-base /usr/local/bundle/ /usr/local/bundle/
...

to copy the gems I installed in the previous stage, and then proceed with the rest of the application handling:

...
ADD app.rb /app/app.rb
ADD Gemfile /app/Gemfile
ADD views/index.erb /app/views/index.erb
ADD views/confirmation.erb /app/views/confirmation.erb
ADD environment.rb /app/environment.rb
ENTRYPOINT ["ruby","app.rb","-o","0.0.0.0"]

to complete the image I'll ultimately run.

Going forward, unless you need to update your base image (which this allows you to handle as a separate task), you can target the new application only image stage rb-app (i.e. in a CI system that builds and pushes to your registry):

docker build --target rb-app -t coolregistryusa.biz/jmarhee/app:latest .

and make the image available from the registry per usual from there.

Top comments (0)