Việc áp dụng Docker vào dự án hẳn không phải là điều gì quá xa lạ với developer. Docker giúp chúng ta tối ưu công việc cài đặt môi trường trên các máy cần chạy project, từ development cho tới production. Tuy nhiên sử dụng Docker như thế nào cho hợp lý và tối ưu thì không phải ai cũng làm được. Bài viết hôm nay mình xin giới thiệu cách cache lại Bundle của ứng dụng Rails khi bạn config Docker sử dụng Docker Compose
Đặt vấn đề
Mỗi lần Gemfile thay đổi, đồng nghĩa với việc phải build lại Docker container để load những sự thay đổi đó, cũng có nghĩa là mỗi lần rebuild sẽ phải chạy lại bundle install
từ đầu, bởi vì Docker sẽ build container mới chứ không ghi đè những thay đổi trên container có sẵn. Khi app của bạn dùng ít gem và những gem của bạn tốn ít thời gian để install thì có thể bạn sẽ cảm thấy no problem. Tuy nhiên khi app càng phình to, nhiều gem được thêm mới hoặc thay đổi version, mỗi lần rebuild container đồng nghĩa bạn sẽ phải ngồi đợi cả thế kỷ, trong khi nếu không dùng Docker thì bạn chỉ sẽ phải đợi install những Gem có sự thay đổi. Vậy thì vấn đề này sẽ xử lý như thế nào khi bạn làm việc với Docker
Hướng giải quyết
Khi bạn build lại Container, tất cả sẽ được làm mới, chính vì vậy việc đầu tiên sẽ phải lưu trữ những gem đã được install vào một nơi khác chứ không phải là container. Khi build một container, chỉ cần thực hiện check Gemfile có gì thay đổi không, nếu có thì chỉ install lại những gem đó, bỏ qua những gem đã được install.
Vậy thì chúng ta sẽ lưu trữ những gem đã install ở đâu? Ở một folder nào đó của máy chủ đang chạy Docker? Không, Docker cung cấp cho chúng ta một công cụ tên là Docker Volume. Docker Volume có thể tưởng tượng nó như là một cái database độc lập, không liên quan gì đến vòng đời của container, nó sẽ luôn ở đó từ khi chúng ta thực hiện khởi tạo, lưu mọi dữ liệu chúng ta cần và chỉ biến mất khi chúng ta thực sự xóa nó.
Bắt đầu config Docker của app chúng ta với Dockerfile đơn giản như sau:
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 |
Điều quan trọng nhất ở đây chính là set những biến môi trường BUNDLE_PATH
, BUNDLE_BIN
, GEM_HOME
vào folder /bundle
, nhằm khai báo cho Ruby biết bạn muốn lưu trữ các gem được cài đặt trong container vào folder này. Hoặc nếu không muốn, mặc định nó sẽ lưu trong /usr/local/bundle
. Đường dẫn tới folder này rất quan trọng cho bước tiếp theo.
Với Dockerfile trên, chúng ta hoàn toàn có thể build một image giúp app chúng ta chạy trên đó. Tiếp theo chúng ta sẽ định nghĩa Docker Compose để tiến hành chạy app vào image được tạo bởi Dockerfile vừa viết.
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> |
Chúng ta để ý dòng config sau:
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 |
Chúng ta khai báo với Docker sẽ sử dụng Volume, Volume được định nghĩa dưới tên bundle
sẽ được mount vào folder /bundle
trong container, chính là folder mà chúng ta đã định nghĩa ở Dockerfile. Với config này mỗi lần rebuild container, các gem đã cài đặt trước đó sẽ được mount từ Volume vào container, nên ta sẽ không mất thời gian install lại nữa.
Vậy thì việc check bundle đã tồn tại và bundle install gem mới sẽ nằm ở đâu?
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 |
Ở đây có 2 file là:
wait-for-it.sh
: Đây đơn thuần chỉ là 1 file đợi, mục đích là đợi db ở port 3306 được khởi tạo thành công thì mới chạy filedocker/app/entrypoint.sh
entrypoint.sh
: file này sẽ chứa các lệnh mà mỗi lần deploy sẽ tự động chạy, ví dụ như: migrate db, generate API docs, bundle install, … và khởi động 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 href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="193d59">[email protected]</a></span>"</span> |
Ở dòng lệnh thứ 3, chúng ta sẽ thực hiện bundle check
, lúc này các gem đã cài đặt đã được mount từ volume vào folder /bundle trong container nên sẽ không thực hiện install những gem này nữa. Sau khi thực hiện check có thay đổi thì sẽ thực hiện bundle install
những gem có sự thay đổi.
Với những cài đặt trên, chúng ta chỉ cần build images 1 lần bằng lệnh docker-compose build
.
Và sau đó mỗi lần deploy chỉ cần docker-compose down
để kết thúc bản build cũ và docker-compose up
để khởi tạo bản build với code mới. Và tất nhiên, sẽ không còn cảnh chờ đợi bundle install lại từ đầu như vấn đề mình đã nêu ra trước đó. Hi vọng bài viết sẽ giúp ích cho các bạn