Applying Docker to the project must not be too strange for the developer. Docker helps us optimize the installation environment on machines that need to run the project, from development to production. However, using Docker properly and optimally is not possible for everyone. Today I introduce how to cache the Bundle of the Rails application when you configure Docker using Docker Compose
Question
Every time Gemfile changes, it means that the Docker container must be rebuilt to load those changes, which means that each rebuild will have to run bundle install
again from the beginning, because Docker will build the new container rather than overwrite it. Changes on container are available. When your app uses few gems and your gems take a little time to install, you may feel no problem. However, when the app gets bigger, more gems are added or the version is changed, each rebuilding the container means you will have to wait for a century, while if you do not use Docker, you will only have to wait for the gems to install. change. So how does this work when you work with Docker?
Solution
When you rebuild a Container, everything will be refreshed, so the first thing to do is to store the gems that have been installed in a different place than the container. When building a container, just check Gemfile if anything changes, if any, just reinstall those gems, ignoring the gems already installed.
So where do we store the installed gems? In a certain folder of the server running Docker? No, Docker provides us with a tool called Docker Volume. Docker Volume can imagine it as a standalone database, regardless of the container’s lifecycle, it will always be there when we initialize, save all the data we need and just disappear. when we actually delete it.
Starting our app’s Docker config with Dockerfile is as simple as:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<span class="token comment"># Sử dụng Ruby version 2.6.5</span> <span class="token keyword">FROM</span> ruby <span class="token punctuation">:</span> 2.6.5 <span class="token comment"># Thực hiện cài đặt một số package cơ bản</span> <span class="token keyword">RUN</span> apt <span class="token punctuation">-</span> get update && apt <span class="token punctuation">-</span> get install <span class="token punctuation">-</span> y build <span class="token punctuation">-</span> essential curl cron logrotate gettext <span class="token punctuation">-</span> base nano <span class="token keyword">RUN</span> apt <span class="token punctuation">-</span> get clean <span class="token comment"># Tạo thư mục làm việc là /app</span> <span class="token keyword">RUN</span> mkdir <span class="token punctuation">-</span> p /app <span class="token keyword">WORKDIR</span> /app <span class="token comment"># Định nghĩa path lưu các gem được cài đặt</span> <span class="token keyword">ENV</span> BUNDLE_PATH=/bundle BUNDLE_BIN=/bundle/bin GEM_HOME=/bundle <span class="token keyword">ENV</span> PATH= <span class="token string">"${BUNDLE_BIN}:${PATH}"</span> <span class="token comment"># Expose app ra port 3000 trong container</span> <span class="token keyword">EXPOSE</span> 3000 |
The most important thing here is to put the environment variables BUNDLE_PATH
, BUNDLE_BIN
, GEM_HOME
into the folder /bundle
, to tell Ruby that you want to store the gems installed in the container into this folder. Or if you don’t want to, it will be saved in /usr/local/bundle
default. The path to this folder is important for the next step.
With Dockerfile above, we can completely build an image to help our app run on it. Next we will define Docker Compose to run the app on the image created by Dockerfile just written.
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 |
<span class="token key atrule">version</span> <span class="token punctuation">:</span> <span class="token string">"3"</span> <span class="token key atrule">services</span> <span class="token punctuation">:</span> <span class="token key atrule">db</span> <span class="token punctuation">:</span> <span class="token key atrule">image</span> <span class="token punctuation">:</span> mysql <span class="token punctuation">:</span> 8.0.13 <span class="token comment"># Sử dụng offical image mysql version 8.0.13</span> <span class="token key atrule">volumes</span> <span class="token punctuation">:</span> <span class="token punctuation">-</span> db <span class="token punctuation">-</span> data <span class="token punctuation">:</span> /var/lib/mysql <span class="token comment"># Sử dụng volume để lưu dữ liệu tránh mất mát mỗi lần rebuild container</span> <span class="token key atrule">env_file</span> <span class="token punctuation">:</span> .env <span class="token key atrule">networks</span> <span class="token punctuation">:</span> <span class="token punctuation">-</span> my_docker <span class="token key atrule">app</span> <span class="token punctuation">:</span> <span class="token key atrule">build</span> <span class="token punctuation">:</span> <span class="token comment"># Sử dụng images build từ dockerfile</span> <span class="token key atrule">context</span> <span class="token punctuation">:</span> . <span class="token key atrule">dockerfile</span> <span class="token punctuation">:</span> docker/ruby/Dockerfile <span class="token comment"># Trỏ tới Dockerfile vừa định nghĩa</span> <span class="token key atrule">command</span> <span class="token punctuation">:</span> docker/common/wait <span class="token punctuation">-</span> for <span class="token punctuation">-</span> it.sh db <span class="token punctuation">:</span> 3306 <span class="token punctuation">-</span> <span class="token punctuation">-</span> docker/app/entrypoint.sh <span class="token comment"># command sau khi build service thành công</span> <span class="token key atrule">volumes</span> <span class="token punctuation">:</span> <span class="token punctuation">-</span> . <span class="token punctuation">:</span> /app <span class="token punctuation">-</span> bundle <span class="token punctuation">:</span> /bundle <span class="token key atrule">ports</span> <span class="token punctuation">:</span> <span class="token punctuation">-</span> 3000 <span class="token punctuation">:</span> <span class="token number">3000</span> <span class="token key atrule">env_file</span> <span class="token punctuation">:</span> .env <span class="token key atrule">stdin_open</span> <span class="token punctuation">:</span> <span class="token boolean important">true</span> <span class="token key atrule">tty</span> <span class="token punctuation">:</span> <span class="token boolean important">true</span> <span class="token key atrule">networks</span> <span class="token punctuation">:</span> <span class="token punctuation">-</span> my_docker <span class="token key atrule">volumes</span> <span class="token punctuation">:</span> <span class="token key atrule">db-data</span> <span class="token punctuation">:</span> <span class="token key atrule">bundle</span> <span class="token punctuation">:</span> <span class="token key atrule">networks</span> <span class="token punctuation">:</span> <span class="token key atrule">demo_docker</span> <span class="token punctuation">:</span> <span class="token key atrule">external</span> <span class="token punctuation">:</span> <span class="token key atrule">name</span> <span class="token punctuation">:</span> my_docker <span class="token comment"># define network để các container connect với nhau</span> |
We notice the following config:
1 2 3 |
<span class="token key atrule">volumes</span> <span class="token punctuation">:</span> <span class="token punctuation">-</span> bundle <span class="token punctuation">:</span> /bundle |
We declare to Docker that we will use the Volume, the Volume defined under the bundle
name will be mounted in the folder /bundle
in the container, which is the folder that we have defined in Dockerfile. With this config every rebuild container, the previously installed gems will be mounted from the Volume into the container, so we will not take time to reinstall again.
So, where does the check bundle already exist and where the new bundle install gem will be?
1 2 |
<span class="token key atrule">command</span> <span class="token punctuation">:</span> docker/common/wait <span class="token punctuation">-</span> for <span class="token punctuation">-</span> it.sh db <span class="token punctuation">:</span> 3306 <span class="token punctuation">-</span> <span class="token punctuation">-</span> docker/app/entrypoint.sh |
There are 2 files here:
wait-for-it.sh
: This is just a wait file, the purpose is to wait for the db in port 3306 to be successfully created to run the filedocker/app/entrypoint.sh
entrypoint.sh
: this file will contain commands that each deploy will automatically run, such as: migrate db, generate API docs, bundle install , … and start the rails server
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<span class="token shebang important">#!/bin/bash</span> <span class="token keyword">set</span> -e bundle check <span class="token operator">||</span> bundle <span class="token function">install</span> --binstubs <span class="token operator">=</span> <span class="token string">" <span class="token variable">$BUNDLE_BIN</span> "</span> bundle <span class="token function">exec</span> rake db:create bundle <span class="token function">exec</span> rake db:migrate <span class="token function">rm</span> -f tmp/pids/server.pid bundle <span class="token function">exec</span> rails assets:precompile bundle <span class="token function">exec</span> rails server -b 0.0.0.0 <span class="token function">exec</span> <span class="token string">" <span class="token variable"><a class="__cf_email__" href="/cdn-cgi/l/email-protection" data-cfemail="193d59">[email protected]</a></span> "</span> |
On the third command line, we will perform the bundle check
, now the installed gems are mounted from the volume to the folder / bundle in the container so they will not install these gems anymore. After performing the check that has changed, we will perform bundle install
the changed gems.
With the above settings, we only need to build images once using the docker-compose build
command.
And then each time deploy just docker-compose down
to finish the old build and docker-compose up
to initialize the build with the new code. And of course, there will be no more scenes of waiting for bundle to reinstall from the beginning as the problem I raised before. Hope the article will help you