Ruby on Rails — Smaller docker images
When building docker containers, it is best practice to build small container images. Docker images that are smaller in size tend to have faster build times and pull times. Also, there are security benefits with using small docker images — small containers have a small attack surface compared to large docker containers.
How small should a rails docker container be? It depends. Every application is different. My rule of thumb is to keep container size to ~300MB (or ~100MB when compressed). It is easy for a fully featured rails application docker image to have a size of about 1GB! Here is an example:
I have created a new application using the http://jumpstartrails.com/ template
rails new myapp -d postgresql -m https://raw.githubusercontent.com/excid3/jumpstart/master/template.rb
Using a basic Dockerfile with a ruby:2.5.3 base image:
FROM ruby:2.5.1RUN apt-get update \
&& apt-get install -y apt-transport-https \
&& curl --silent --show-error --location \
https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key
add - \
&& echo "deb https://deb.nodesource.com/node_6.x/ stretch main"
> /etc/apt/sources.list.d/nodesource.list \
&& curl --silent --show-error --location
https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
&& echo "deb https://dl.yarnpkg.com/debian/ stable main" >
/etc/apt/sources.list.d/yarn.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
postgresql-client nodejs apt-transport-https yarn \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*WORKDIR /usr/src/app
ENV RAILS_ENV=production
ENV NODE_ENV=productionCOPY Gemfile* package.json yarn.lock ./
RUN bundle install
RUN yarn install
COPY . .
RUN bin/rails assets:precompileEXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]
We end up with a docker image of size:
REPOSITORY TAG SIZE
small-docker-images v1 1.48GB
So how do you keep the docker image size under control?
1. Use a smaller base image
In the first version, v1
, we used ruby:2.5.1
as the base image which was very convenient but also large in size.
REPOSITORY TAG SIZE
ruby 2.5.1 869MB
Starting with a smaller base image can help reduce the size of the final image. An example of a smaller base image is ruby-alpine
:
REPOSITORY TAG SIZE
ruby 2.5.1-alpine 45.3MB
That’s about 820MB removed!
Using an Alpine image as the base image could be a bit more involved, you have to run a few commands and install a few more packages:
FROM ruby:2.5.1-alpineRUN apk update \
&& apk upgrade \
&& apk add --update --no-cache \
build-base curl-dev git postgresql-dev \
yaml-dev zlib-dev nodejs yarnWORKDIR /usr/src/app
ENV RAILS_ENV=production
ENV NODE_ENV=productionCOPY Gemfile* package.json yarn.lock ./
RUN bundle install
RUN yarn install
COPY . .
RUN bin/rails assets:precompileEXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]
After using an alpine base image, we end up with a small final v2
image that’s about half of v1
REPOSITORY TAG SIZE
small-docker-images v2 810MB
But we can reduce this even further!
2. Reduce the number of layers with multi-step docker builds
Just as the size of your git repo increases with the number commits/changes, the size of final docker image depends on the number of layers. You can reduce the number of layers by decreasing the number of RUN
and COPY
statements or by doing a multi-step build which is similar to a “git squash”.
Here’s my new Dockerfile using the multi-step build approach:
FROM ruby:2.5.1-alpine AS build-envARG RAILS_ROOT=/app
ARG BUILD_PACKAGES="build-base curl-dev git"
ARG DEV_PACKAGES="postgresql-dev yaml-dev zlib-dev nodejs yarn"
ARG RUBY_PACKAGES="tzdata"ENV RAILS_ENV=production
ENV NODE_ENV=production
ENV BUNDLE_APP_CONFIG="$RAILS_ROOT/.bundle"WORKDIR $RAILS_ROOT# install packages
RUN apk update \
&& apk upgrade \
&& apk add --update --no-cache $BUILD_PACKAGES $DEV_PACKAGES \
$RUBY_PACKAGESCOPY Gemfile* package.json yarn.lock ./
RUN bundle config --global frozen 1 \
&& bundle install --path=vendor/bundle
RUN yarn install
COPY . .
RUN bin/rails assets:precompile############### Build step done ###############FROM ruby:2.5.1-alpine
ARG RAILS_ROOT=/app
ARG PACKAGES="tzdata postgresql-client nodejs bash"ENV RAILS_ENV=production
ENV BUNDLE_APP_CONFIG="$RAILS_ROOT/.bundle"WORKDIR $RAILS_ROOT# install packages
RUN apk update \
&& apk upgrade \
&& apk add --update --no-cache $PACKAGESCOPY --from=build-env $RAILS_ROOT $RAILS_ROOT
EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]
And this reduced the size by about half!
REPOSITORY TAG SIZE
small-docker-images v3 360MB
This is around my ideal docker size but there is still a bit of cleaning up that can be done to reduce the size further.
3. Remove unnecessary files during the build
Some files that are used in development are never used in production, a few examples are:
1. app/assets vendor/assets node_modules - If you precompile your assets/webpack, these folders become irrelevant in prod2. tmp/cache spec are also not needed in production3. remove the gems cache and *.c and *.o files
After removing these files, this is my new docker file:
FROM ruby:2.5.1-alpine AS build-envARG RAILS_ROOT=/app
ARG BUILD_PACKAGES="build-base curl-dev git"
ARG DEV_PACKAGES="postgresql-dev yaml-dev zlib-dev nodejs yarn"
ARG RUBY_PACKAGES="tzdata"ENV RAILS_ENV=production
ENV NODE_ENV=production
ENV BUNDLE_APP_CONFIG="$RAILS_ROOT/.bundle"WORKDIR $RAILS_ROOT# install packages
RUN apk update \
&& apk upgrade \
&& apk add --update --no-cache $BUILD_PACKAGES $DEV_PACKAGES $RUBY_PACKAGESCOPY Gemfile* package.json yarn.lock ./
# install rubygem
COPY Gemfile Gemfile.lock $RAILS_ROOT/
RUN bundle config --global frozen 1 \
&& bundle install --without development:test:assets -j4 --retry 3 --path=vendor/bundle \
# Remove unneeded files (cached *.gem, *.o, *.c)
&& rm -rf vendor/bundle/ruby/2.5.0/cache/*.gem \
&& find vendor/bundle/ruby/2.5.0/gems/ -name "*.c" -delete \
&& find vendor/bundle/ruby/2.5.0/gems/ -name "*.o" -deleteRUN yarn install --production
COPY . .
RUN bin/rails webpacker:compile
RUN bin/rails assets:precompile# Remove folders not needed in resulting image
RUN rm -rf node_modules tmp/cache app/assets vendor/assets spec############### Build step done ###############FROM ruby:2.5.1-alpine
ARG RAILS_ROOT=/app
ARG PACKAGES="tzdata postgresql-client nodejs bash"ENV RAILS_ENV=production
ENV BUNDLE_APP_CONFIG="$RAILS_ROOT/.bundle"WORKDIR $RAILS_ROOT# install packages
RUN apk update \
&& apk upgrade \
&& apk add --update --no-cache $PACKAGESCOPY --from=build-env $RAILS_ROOT $RAILS_ROOT
EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]
And the size comes to about
REPOSITORY TAG SIZE
small-docker-images v4 162MB
That’s pretty good considering the starting size of 1.5G.
Building small docker containers can improve performance in builds/pulls and security of the application. It’s easy to start reducing the size of your rails docker image by utilizing small base images and the multi-step build pattern.