[Ruby on Rails (RoR)] Reduce and optimize Ruby on Rails, PostgreSQL Docker Image size

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.

1
2
FROM ruby:alpine
# FROM ruby:3.0.2-alpine

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
# Dockerfile

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=/app
RUN mkdir -p ${APP_HOME}
ENV APP_HOME ${APP_HOME}
WORKDIR ${APP_HOME}

ARG RAILS_ENV=production
ENV 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
# Dockerfile-builder-alpine

ARG RUBY_VERSION=base-3.0.2-alpine
FROM cloudolife/ruby-postgresql:${RUBY_VERSION}

LABEL maintainer="Benjamin CloudoLife <[email protected]>"

RUN apk --no-cache --update add \
build-base \
git \
# nodejs \
openssh \
postgresql-dev
# yarn

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
# Dockerfile

# Stage Build
ARG RUBY_VERSION=builder-3.0.2-alpine
FROM cloudolife/ruby-postgresql:${RUBY_VERSION} AS Builder

LABEL maintainer="Benjamin CloudoLife <[email protected]>"

COPY . .

RUN bundle config set --local without 'development test' && \
bundle -j3 --retry 3

# Stage Run
ARG RUBY_VERSION=base-3.0.2-alpine
FROM cloudolife/ruby-postgresql:${RUBY_VERSION}

LABEL maintainer="Benjamin CloudoLife <[email protected]>"

# Copy app with gems from build stage
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
# Dockerfile-base-alpine

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=/app
RUN mkdir -p ${APP_HOME}
ENV APP_HOME ${APP_HOME}
WORKDIR ${APP_HOME}

ARG RAILS_ENV=production
ENV 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
# Dockerfile-builder-alpine

ARG RUBY_VERSION=base-3.0.2-alpine

FROM 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
# Dockerfile

# Stage Build
ARG RUBY_VERSION=builder-3.0.2-alpine
FROM cloudolife/ruby-postgresql:${RUBY_VERSION} AS Builder

LABEL maintainer="Benjamin CloudoLife <[email protected]>"

# COPY Gemfile* package.json yarn.lock ./
# RUN bundle --without development test -j3 --retry 3
RUN yarn install

COPY . .

RUN bundle config set --local without 'development test' && \
bundle -j3 --retry 3

RUN bin/rails db:migrate
# RUN bin/rails assets:precompile


# Stage Run
ARG RUBY_VERSION=base-3.0.2-alpine
FROM cloudolife/ruby-postgresql:${RUBY_VERSION}

LABEL maintainer="Benjamin CloudoLife <[email protected]>"

# Copy app with gems from build stage
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/