Reduce and optimize Ruby on Rails, PostgreSQL Docker Image size
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-stage builds pattern.
Prerequisites
Create a Rails project with PostgreSQL.
1 $ rails new myapp -d postgresql
Ingore files by .dockerignore
you should add a .dockerignore
file to your code base, which works pretty much the same way the .gitignore
file does. In addition to copying over the contents of your .gitignore
file, you might want to include the .git/
directory as well to the list of files ignored by Docker.
1 2 3 4 5 6 7 .dockerignore .git/ Dockerfile log/ tmp/
See .dockerignore file | Dockerfile reference | Docker Documentation - https://docs.docker.com/engine/reference/builder/#dockerignore-file to learn more.
Mininal Ruby Image - Ruby Alpine
Alpine Linux is a Linux distribution built around musl libc and BusyBox. The image is only 5 MB in size and has access to a package repository that is much more complete than other BusyBox based images. This makes Alpine Linux a great image base for utilities and even production applications. Read more about Alpine Linux here - https://alpinelinux.org/about/ and you can see how their mantra fits in right at home with Docker images.
Multi-stage Builds
Multistage builds are useful to anyone who has struggled to optimize Dockerfiles while keeping them easy to read and maintain.
With multi-stage builds, you 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. To show how this works, let’s adapt the Dockerfile from the previous section to use multi-stage builds.
Make Base Image
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 ARG RUBY_VERSION=3.0 .2 FROM ruby:${RUBY_VERSION}LABEL maintainer="Benjamin CloudoLife <[email protected] >" RUN apk --no-cache --update add \ libcurl \ postgresql-client \ tzdata ARG APP_HOME=/appRUN mkdir -p ${APP_HOME} ENV APP_HOME ${APP_HOME}WORKDIR ${APP_HOME} ARG RAILS_ENV=productionENV RAILS_ENV ${RAILS_ENV}RUN gem update --system COPY entrypoint.sh /usr/sbin/entrypoint.sh ENTRYPOINT ["entrypoint.sh" ] CMD ["bundle" , "exec" , "rails" , "s" , "-b" , "0.0.0.0" ]
Build and tag:
1 $ docker build . -t cloudolife/ruby-postgresql:base-3.0.2-alpine
Make Builder Image
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ARG RUBY_VERSION=base-3.0 .2 -alpineFROM cloudolife/ruby-postgresql:${RUBY_VERSION}LABEL maintainer="Benjamin CloudoLife <[email protected] >" RUN apk --no-cache --update add \ build-base \ git \ openssh \ postgresql-dev
Build and tag:
1 $ docker build . -t cloudolife/ruby-postgresql:builder-3.0.2-alpine
Multiple Stages
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ARG RUBY_VERSION=builder-3.0 .2 -alpineFROM cloudolife/ruby-postgresql:${RUBY_VERSION} AS BuilderLABEL maintainer="Benjamin CloudoLife <[email protected] >" COPY . . RUN bundle config set --local without 'development test' && \ bundle -j3 --retry 3 ARG RUBY_VERSION=base-3.0 .2 -alpineFROM cloudolife/ruby-postgresql:${RUBY_VERSION}LABEL maintainer="Benjamin CloudoLife <[email protected] >" COPY --from=Builder /usr/local/bundle/ /usr/local/bundle/ COPY --from=Builder /app /app RUN bin/rails db:migrate
Build and tag:
1 $ docker build . -t cloudolife/ruby-postgresql-example:3.0.2-alpine
Remove Cache and unnecessary file
1 2 3 RUN rm -rf /usr/local/bundle/cache/*.gem && \ find /usr/local/bundle/gems/ -name "*.c" -delete && \ find /usr/local/bundle/gems/ -name "*.o" -delete
Build and tag:
1 $ docker build . -t cloudolife/ruby-postgresql-example:3.0.2-alpine
Conclusion
Dockerfile-base-alpine:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 ARG RUBY_VERSION=3.0 .2 FROM ruby:${RUBY_VERSION}LABEL maintainer="Benjamin CloudoLife <[email protected] >" RUN apk --no-cache --update add \ libcurl \ postgresql-client \ tzdata ARG APP_HOME=/appRUN mkdir -p ${APP_HOME} ENV APP_HOME ${APP_HOME}WORKDIR ${APP_HOME} ARG RAILS_ENV=productionENV RAILS_ENV ${RAILS_ENV}RUN gem update --system COPY entrypoint.sh /usr/sbin/entrypoint.sh ENTRYPOINT ["entrypoint.sh" ] CMD ["bundle" , "exec" , "rails" , "s" , "-b" , "0.0.0.0" ]
Dockerfile-builder-alpine
1 2 3 4 5 6 7 8 9 10 11 12 13 ARG RUBY_VERSION=base-3.0 .2 -alpineFROM cloudolife/ruby-postgresql:${RUBY_VERSION}LABEL maintainer="Benjamin CloudoLife <[email protected] >" RUN apk --no-cache --update add \ build-base \ git \ openssh \ postgresql-dev
ruby-postgresql-example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 ARG RUBY_VERSION=builder-3.0 .2 -alpineFROM cloudolife/ruby-postgresql:${RUBY_VERSION} AS BuilderLABEL maintainer="Benjamin CloudoLife <[email protected] >" RUN yarn install COPY . . RUN bundle config set --local without 'development test' && \ bundle -j3 --retry 3 RUN bin/rails db:migrate ARG RUBY_VERSION=base-3.0 .2 -alpineFROM cloudolife/ruby-postgresql:${RUBY_VERSION}LABEL maintainer="Benjamin CloudoLife <[email protected] >" COPY --from=Builder /usr/local/bundle/ /usr/local/bundle/ COPY --from=Builder /app /app RUN rm -rf /usr/local/bundle/cache/*.gem && \ find /usr/local/bundle/gems/ -name "*.c" -delete && \ find /usr/local/bundle/gems/ -name "*.o" -delete
1 $ docker build -f Dockerfile . -t cloudolife/ruby-postgresql-example:3.0.2-alpine
Show Image size and history
docker history
Show the history of an image
1 2 Usage docker history [OPTIONS] IMAGE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 docker history cloudolife/ruby-postgresql:builder-3.0.2-alpine IMAGE CREATED CREATED BY SIZE COMMENT 42681291d509 28 hours ago RUN /bin/sh -c apk --no-cache --update add … 523MB buildkit.dockerfile.v0 <missing> 28 hours ago LABEL maintainer=Benjamin CloudoLife <benjam… 0B buildkit.dockerfile.v0 <missing> 28 hours ago CMD ["bundle" "exec" "rails" "s" "-b" "0.0.0… 0B buildkit.dockerfile.v0 <missing> 28 hours ago ENTRYPOINT ["entrypoint.sh"] 0B buildkit.dockerfile.v0 <missing> 28 hours ago COPY entrypoint.sh /usr/sbin/entrypoint.sh #… 173B buildkit.dockerfile.v0 <missing> 28 hours ago RUN |2 APP_HOME=/app RAILS_ENV=production /b… 24.2MB buildkit.dockerfile.v0 <missing> 28 hours ago ENV RAILS_ENV=production 0B buildkit.dockerfile.v0 <missing> 28 hours ago ARG RAILS_ENV=production 0B buildkit.dockerfile.v0 <missing> 28 hours ago WORKDIR /app 0B buildkit.dockerfile.v0 <missing> 28 hours ago ENV APP_HOME=/app 0B buildkit.dockerfile.v0 <missing> 28 hours ago RUN |1 APP_HOME=/app /bin/sh -c mkdir -p ${A… 0B buildkit.dockerfile.v0 <missing> 28 hours ago ARG APP_HOME=/app 0B buildkit.dockerfile.v0 <missing> 28 hours ago RUN /bin/sh -c apk --no-cache --update add … 6.12MB buildkit.dockerfile.v0 <missing> 28 hours ago LABEL maintainer=Benjamin CloudoLife <benjam… 0B buildkit.dockerfile.v0 <missing> 3 days ago /bin/sh -c #(nop) CMD ["irb"] 0B <missing> 3 days ago /bin/sh -c mkdir -p "$GEM_HOME" && chmod 777… 0B <missing> 3 days ago /bin/sh -c #(nop) ENV PATH=/usr/local/bundl… 0B <missing> 3 days ago /bin/sh -c #(nop) ENV BUNDLE_SILENCE_ROOT_W… 0B <missing> 3 days ago /bin/sh -c #(nop) ENV GEM_HOME=/usr/local/b… 0B <missing> 3 days ago /bin/sh -c set -eux; apk add --no-cache --… 43.4MB <missing> 2 months ago /bin/sh -c #(nop) ENV RUBY_DOWNLOAD_SHA256=… 0B <missing> 2 months ago /bin/sh -c #(nop) ENV RUBY_VERSION=3.0.2 0B <missing> 2 months ago /bin/sh -c #(nop) ENV RUBY_MAJOR=3.0 0B <missing> 2 months ago /bin/sh -c #(nop) ENV LANG=C.UTF-8 0B <missing> 2 months ago /bin/sh -c set -eux; mkdir -p /usr/local/et… 45B <missing> 2 months ago /bin/sh -c set -eux; apk add --no-cache b… 11.4MB <missing> 2 months ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B <missing> 2 months ago /bin/sh -c #(nop) ADD file:aad4290d27580cc1a… 5.6MB
See docker history | Docker Documentation - https://docs.docker.com/engine/reference/commandline/history/ to learn more.
Dive
Dive is a tool for exploring a docker image, layer contents, and discovering ways to shrink the size of your Docker/OCI image.
1 $ dive cloudolife/ruby-postgresql:builder-3.0.2-alpine
See wagoodman/dive: A tool for exploring each layer in a docker image - https://github.com/wagoodman/dive to leran more.
Compare Image Size
Image
Size
Description
ruby:3.0.2-bullseye
881MB
Ruby 3.0.2 Official Bullseye Image
ruby:3.0.2-slim-bullseye
163MB
Ruby 3.0.2 Official Slim Bullseye Image
alpine
5.6MB
Alpine Official Image
ruby:3.0.2-alpine
60.4MB
Ruby 3.0.2 Official Alpine Image
cloudolife/ruby-postgresql:base-3.0.2-alpine
90.7MB
Ruby on Rails, PostgreSQL Client Alpine Image
cloudolife/ruby-postgresql:builder-3.0.2-alpine
614MB
Ruby on Rails, Base Build and PostgreSQL Dev Alpine Image
cloudolife/ruby-postgresql-example:3.0.2-alpine
275MB
ruby-postgresql-example Alpine Image
FAQs
failed to solve with frontend dockerfile.v0: failed to create LLB definition: circular dependency detected on stage: vendor
1 2 failed to solve with frontend dockerfile.v0: failed to create LLB definition: circular dependency detected on stage: vendor make: *** [docker-build-tag] Error 1
TODO
References
[1] Building Docker images, the performant way | Georg Ledermann, Full Stack Developer: Ruby on Rails, Vue.js - https://ledermann.dev/blog/2020/01/29/building-docker-images-the-performant-way/
[2] Best practices when writing a Dockerfile for a Ruby application | Florin Lipan - https://lipanski.com/posts/dockerfile-ruby-best-practices
[3] Dockerize Rails, the lean way | Georg Ledermann, Full Stack Developer: Ruby on Rails, Vue.js - https://ledermann.dev/blog/2018/04/19/dockerize-rails-the-lean-way/
[4] Ruby on Rails — Smaller docker images | by Lemuel Barango | Medium - https://medium.com/@lemuelbarango/ruby-on-rails-smaller-docker-images-bff240931332
[5] Reduce your Docker images (an example with Ruby) - DEV Community - https://dev.to/caduribeiro/reduce-your-docker-images-an-example-with-ruby-30db
[6] Alpine - Official Image | Docker Hub - https://hub.docker.com/_/alpine
[7] Ruby - Official Image | Docker Hub - https://hub.docker.com/_/ruby
[8] Use multi-stage builds | Docker Documentation - https://docs.docker.com/develop/develop-images/multistage-build/
[9] .dockerignore file | Dockerfile reference | Docker Documentation - https://docs.docker.com/engine/reference/builder/#dockerignore-file
[10] docker history | Docker Documentation - https://docs.docker.com/engine/reference/commandline/history/
[11] wagoodman/dive: A tool for exploring each layer in a docker image - https://github.com/wagoodman/dive
[12] Empowering App Development for Developers | Docker - https://www.docker.com/