Nếu như bạn là một developer đã từng có chân trong nhiều dự án cùng lúc thì chắc hẳn bạn cũng đã từng gặp phải những vấn đề khó chịu liên quan đến phiên bản hay thư viện được sử dụng trong mỗi dự án đó. Để rồi mỗi khi chuyển đổi qua lại giữa các môi trường bạn lại phải mất công setup lại đủ thứ. Hay đơn giản hơn là việc code trên máy công ty thì chạy, nhưng khi về đến nhà, bạn mở máy ra code những đoạn code còn dang dở thì lại không hiểu tại sao nó lại không thể chạy được nữa. Đó cũng chính là lúc bạn sẽ nghĩ đến docker. Với khẩu hiệu “build once, run anywhere“, chắn chắn những vấn đề trên sẽ không còn khiến bạn phải đau đầu thêm nữa. Ngày hôm nay chúng ta sẽ cùng nhau dựng một ứng dụng Ruby on Rails trên môi trường Docker.
Docker compose
Trước khi đi vào vấn đề chính, chúng ta cần phải xác định rõ những container nào sẽ được tạo cũng như phuơng thức giao tiếp giữa chúng.
- Đầu tiền là container
app
có nhiệm vụ xử lý logic chính của ứng dụng. - Tiếp theo là container
db
sẽ là nơi lưu trữ dữ liệu. - Cuối cùng là container
nginx
đảm nhận chức năng là web server.
Đó cũng chính là những thành phần cơ bản nhất của một ứng dụng web bất kỳ. Việc xác định những thành phần này cũng hết sức quan trọng, vì nó cho bạn một cái nhìn tổng quan về toàn bộ hệ thống, chức năng của mỗi thành phần để từ đó tối ưu các container cho phù hợp. Dưới đây là nội dung file docker-compose.yml
, nó giống như là một tài liệu chỉ dẫn để build image cũng như quan hệ giữa các container với nhau:
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 35 36 37 38 39 40 41 42 43 44 45 46 47 | <span class="token key atrule">version</span><span class="token punctuation">:</span> <span class="token string">"3.7"</span> <span class="token key atrule">services</span><span class="token punctuation">:</span> <span class="token key atrule">nginx</span><span class="token punctuation">:</span> <span class="token key atrule">build</span><span class="token punctuation">:</span> <span class="token key atrule">context</span><span class="token punctuation">:</span> docker/nginx <span class="token key atrule">dockerfile</span><span class="token punctuation">:</span> Dockerfile <span class="token key atrule">args</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> HOST=$<span class="token punctuation">{</span>HOST<span class="token punctuation">}</span> <span class="token key atrule">depends_on</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> app <span class="token key atrule">ports</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> <span class="token datetime number">80:80</span> <span class="token punctuation">-</span> 443<span class="token punctuation">:</span><span class="token number">443</span> <span class="token key atrule">env_file</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> .env <span class="token key atrule">app</span><span class="token punctuation">:</span> <span class="token key atrule">depends_on</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> db <span class="token key atrule">env_file</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> .env <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">build</span><span class="token punctuation">:</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/app/Dockerfile <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 key atrule">command</span><span class="token punctuation">:</span> sh /scripts/command.sh <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">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><span class="token number">5.7</span> <span class="token key atrule">restart</span><span class="token punctuation">:</span> on<span class="token punctuation">-</span>failure <span class="token key atrule">env_file</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> .env <span class="token key atrule">environment</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> MYSQL_ROOT_PASSWORD=$<span class="token punctuation">{</span>DATABASE_ROOT_PASSWORD<span class="token punctuation">}</span> <span class="token punctuation">-</span> MYSQL_DATABASE=$<span class="token punctuation">{</span>DATABASE_NAME<span class="token punctuation">}</span> <span class="token punctuation">-</span> MYSQL_USER=$<span class="token punctuation">{</span>DATABASE_USER<span class="token punctuation">}</span> <span class="token punctuation">-</span> MYSQL_PASSWORD=$<span class="token punctuation">{</span>DATABASE_PASSWORD<span class="token punctuation">}</span> <span class="token key atrule">volumes</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> mysql_data<span class="token punctuation">:</span>/var/lib/mysql <span class="token key atrule">volumes</span><span class="token punctuation">:</span> <span class="token key atrule">mysql_data</span><span class="token punctuation">:</span> |
Trong file docker-compose.yml
, ở mỗi block chúng ta để ý tới những chi tiết sau:
build
:context
: Đây là nơi định nghĩa nơiDockerfile
sẽ được chạy.dockerfile
: Chỉ định docker file sẽ được sử dụng để build.args
: Các giá trị truyền thêm vào trongDockerfile
depends_on
: Định nghĩa ràng buộc giữa các container với nhau, sử dụng thay thế cholinks
.volumes
: Xác định vùng nhớ của máy chủ sẽ được mount vào trong container.env_file
: Là vị trí file env, bạn có thể dùng các biến env này ở bất cứ đâu trong filedocker-compose.yml
.environment
: Là nơi xác định các biến môi trường trong container.command
: Đây là file sh sẽ được chạy khi container được start.
Dockerfile
Để thuận tiện cho việc quản lý, chúng ta sẽ tạo ra những thư mục tuơng ứng với từng thành phần, với app
và nginx
các thư mục tương ứng của chúng lần lượt là docker/app
, docker/nginx
.
Config app
Dưới đây là nội dung docker/app/Dockerfile
nơi chúng ta sẽ chỉ định những thành phần tạo nên image app
để từ đó build thành container app
:
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 35 36 37 38 | <span class="token keyword">FROM</span> ruby<span class="token punctuation">:</span>2.6.6<span class="token punctuation">-</span>slim<span class="token punctuation">-</span>buster <span class="token keyword">WORKDIR</span> /app <span class="token keyword">COPY</span> Gemfile* ./ <span class="token keyword">COPY</span> package.json ./ <span class="token keyword">COPY</span> yarn.lock ./ <span class="token keyword">RUN</span> apt<span class="token punctuation">-</span>get update && apt<span class="token punctuation">-</span>get install build<span class="token punctuation">-</span>essential <span class="token punctuation">-</span>y <span class="token punctuation">-</span><span class="token punctuation">-</span>no<span class="token punctuation">-</span>install<span class="token punctuation">-</span>recommends gnupg2 curl wget nodejs patch ruby<span class="token punctuation">-</span>dev zlib1g<span class="token punctuation">-</span>dev liblzma<span class="token punctuation">-</span>dev libmariadb<span class="token punctuation">-</span>dev <span class="token keyword">RUN</span> curl <span class="token punctuation">-</span>sS https<span class="token punctuation">:</span>//dl.yarnpkg.com/debian/pubkey.gpg <span class="token punctuation">|</span> apt<span class="token punctuation">-</span>key add <span class="token punctuation">-</span> && echo <span class="token string">"deb https://dl.yarnpkg.com/debian/ stable main"</span> <span class="token punctuation">|</span> tee /etc/apt/sources.list.d/yarn.list && apt<span class="token punctuation">-</span>get update && apt<span class="token punctuation">-</span>get install yarn <span class="token punctuation">-</span><span class="token punctuation">-</span>no<span class="token punctuation">-</span>install<span class="token punctuation">-</span>recommends <span class="token punctuation">-</span>y <span class="token keyword">RUN</span> gem install bundler<span class="token punctuation">:</span>2.1.4 <span class="token keyword">RUN</span> bundle install <span class="token keyword">RUN</span> bundle exec rails db<span class="token punctuation">:</span>prepare <span class="token keyword">RUN</span> yarn install <span class="token punctuation">-</span><span class="token punctuation">-</span>check<span class="token punctuation">-</span>files <span class="token keyword">COPY</span> docker/app/*.sh /scripts/ <span class="token keyword">RUN</span> chmod a+x /scripts/*.sh |
Như đã thấy trong nội dụng file docker-compose.yml, chúng ta sử dụng context .
cho image app, điều này có nghĩa là file docker/app/Dockerfile
sẽ được chạy giống như là nó đang nằm ở ngoài thư mục root của ứng dụng. Đó là lý do vì sao chúng ta có thể sử dụng các lệnh:
1 2 3 4 5 6 | <span class="token keyword">COPY</span> Gemfile* ./ <span class="token keyword">COPY</span> package.json ./ <span class="token keyword">COPY</span> yarn.lock ./ |
Chúng ta cũng sử dụng một file docker/app/command.sh
để chạy các lệnh trước khi khởi động server:
1 2 3 4 5 6 7 8 | <span class="token function">yarn</span> <span class="token function">install</span> --check-files bundle <span class="token function">install</span> bundle <span class="token builtin class-name">exec</span> rails db:prepare db:migrate bundle <span class="token builtin class-name">exec</span> rails s -p <span class="token number">3000</span> -b <span class="token number">0.0</span>.0.0 |
Để ý là trước đó chúng ta đã đưa file này từ bên ngoài vào trong container và set quyền cho nó bằng lệnh:
1 2 3 4 | <span class="token keyword">COPY</span> docker/app/*.sh /scripts/ <span class="token keyword">RUN</span> chmod a+x /scripts/*.sh |
Config Nginx
Đến bước này chúng ta cũng đã có thể chạy ứng dụng của mình được rồi, tuy nhiên để cho mọi thứ gần mới môi trường deploy hơn thì chúng ta cần có thêm một container khác nữa là ngnix
:
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 | <span class="token keyword">FROM</span> nginx<span class="token punctuation">:</span>1.17.9 <span class="token comment"># set environment variables</span> <span class="token keyword">ARG</span> HOST <span class="token keyword">ARG</span> APP_PATH=/app <span class="token comment"># Install dependencies</span> <span class="token keyword">RUN</span> apt<span class="token punctuation">-</span>get update && apt<span class="token punctuation">-</span>get <span class="token punctuation">-</span>y install vim curl openssl apache2<span class="token punctuation">-</span>utils <span class="token punctuation">-</span><span class="token punctuation">-</span>no<span class="token punctuation">-</span>install<span class="token punctuation">-</span>recommends apt<span class="token punctuation">-</span>utils && rm <span class="token punctuation">-</span>r /var/lib/apt/lists/* <span class="token comment"># Set our working directory inside the image</span> <span class="token keyword">WORKDIR</span> $<span class="token punctuation">{</span>APP_PATH<span class="token punctuation">}</span> <span class="token comment"># Copy Nginx config template</span> <span class="token keyword">COPY</span> nginx.conf /tmp/ <span class="token keyword">RUN</span> envsubst <span class="token string">'${APP_PATH} ${HOST}'</span> < /tmp/nginx.conf <span class="token punctuation">></span> /etc/nginx/nginx.conf <span class="token keyword">EXPOSE</span> 80 <span class="token keyword">CMD</span> <span class="token punctuation">[</span><span class="token string">"nginx"</span><span class="token punctuation">,</span> <span class="token string">"-g"</span><span class="token punctuation">,</span> <span class="token string">"daemon off;"</span><span class="token punctuation">]</span> |
Trên đây là nội dung trong file docker/nginx/Dockerfile
, nó là các chỉ thị để build những thành phần cần thiết cho container nginx
. Tiếp theo chúng ta sẽ chuẩn bị file docker/nginx/nginx.conf
để config cho nginx với nội dung sau:
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 35 36 37 38 39 40 | <span class="token keyword">user</span> nginx<span class="token punctuation">;</span> <span class="token keyword">worker_processes</span> <span class="token number">1</span><span class="token punctuation">;</span> <span class="token keyword">error_log</span> <span class="token operator">/</span>var<span class="token operator">/</span>log<span class="token operator">/</span>nginx<span class="token operator">/</span>error<span class="token punctuation">.</span>log warn<span class="token punctuation">;</span> <span class="token keyword">pid</span> <span class="token operator">/</span>var<span class="token operator">/</span>run<span class="token operator">/</span>nginx<span class="token punctuation">.</span><span class="token keyword">pid</span><span class="token punctuation">;</span> <span class="token keyword">events</span> <span class="token punctuation">{</span> <span class="token keyword">worker_connections</span> <span class="token number">1024</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">http</span> <span class="token punctuation">{</span> <span class="token keyword">root</span> <span class="token variable">$APP_PATH</span><span class="token operator">/</span>public<span class="token punctuation">;</span> <span class="token keyword">keepalive_timeout</span> <span class="token number">65</span><span class="token punctuation">;</span> <span class="token keyword">include</span> <span class="token operator">/</span>etc<span class="token operator">/</span>nginx<span class="token operator">/</span>mime<span class="token punctuation">.</span><span class="token keyword">types</span><span class="token punctuation">;</span> <span class="token keyword">default_type</span> application<span class="token operator">/</span>octet<span class="token operator">-</span>stream<span class="token punctuation">;</span> <span class="token keyword">access_log</span> <span class="token operator">/</span>var<span class="token operator">/</span>log<span class="token operator">/</span>nginx<span class="token operator">/</span>access<span class="token punctuation">.</span>log main<span class="token punctuation">;</span> <span class="token keyword">error_log</span> <span class="token operator">/</span>var<span class="token operator">/</span>log<span class="token operator">/</span>nginx<span class="token operator">/</span>error<span class="token punctuation">.</span>log warn<span class="token punctuation">;</span> <span class="token keyword">sendfile</span> on<span class="token punctuation">;</span> <span class="token keyword">upstream</span> <span class="token variable">$HOST</span> <span class="token punctuation">{</span> <span class="token keyword">server</span> app<span class="token punctuation">:</span><span class="token number">3000</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">server</span> <span class="token punctuation">{</span> <span class="token keyword">listen</span> <span class="token number">80</span><span class="token punctuation">;</span> <span class="token keyword">server_name</span> localhost <span class="token number">127.0</span><span class="token number">.0</span><span class="token number">.1</span><span class="token punctuation">;</span> <span class="token keyword">location</span> <span class="token operator">/</span> <span class="token punctuation">{</span> <span class="token keyword">proxy_pass</span> <span class="token keyword">http</span><span class="token punctuation">:</span><span class="token operator">/</span><span class="token operator">/</span><span class="token variable">$HOST</span><span class="token punctuation">;</span> <span class="token keyword">proxy_set_header</span> X<span class="token operator">-</span>Forwarded<span class="token operator">-</span>For <span class="token variable">$remote_addr</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token keyword">include</span> <span class="token operator">/</span>etc<span class="token operator">/</span>nginx<span class="token operator">/</span>conf<span class="token punctuation">.</span>d<span class="token operator">/</span><span class="token operator">*</span><span class="token punctuation">.</span>conf<span class="token punctuation">;</span> <span class="token punctuation">}</span> |
Chúng ta copy file này vào trong thư mục /tmp/ của container bằng lệnh:
1 2 | <span class="token keyword">COPY</span> nginx.conf /tmp/ |
Vì Nginx không hỗ trợ chúng ta truyền các biến môi trường vào trong file config, do đó để khắc phục điều này, chúng ta đã sử dụng envsubst
để replace nội dung trong file /tmp/nginx.conf
trước khi đưa nó vào trong /etc/nginx/nginx.conf
:
1 2 | <span class="token keyword">RUN</span> envsubst <span class="token string">'${APP_PATH} ${HOST}'</span> < /tmp/nginx.conf <span class="token punctuation">></span> /etc/nginx/nginx.conf |
Build
Chuẩn bị một file .env
để lưu tất cả biến môi trường sử dụng trong ứng dụng, nó trông sẽ như thế này:
1 2 3 4 5 6 7 8 9 | HOST=localhost DATABASE_HOST=localhost DATABASE_ROOT_PASSWORD=root DATABASE_NAME=rails_docker DATABASE_PASSWORD=root DATABASE_USER=root |
Mọi thứ có vẻ ổn, giờ là lúc chúng ta tận hưởng thành quả của mình, đầu tiên là build images:
1 2 | docker-compose build |
Nếu như tất cả các image đã được build thành công thì tiếp theo bạn hãy chạy lệnh:
1 2 | docker-compose up |
Mở trình duyệt lên và đánh vào đường dẫn http://localhost nếu như màn hình hiện lên “Yay! You’re on Rails” thì coi như chúng ta đã thành công.
Summary
Vừa rồi chúng ta đã cùng nhau đi dựng một ứng dụng Rails sử dụng Docker. Trên đây mình sử dụng phiên những phiên bản mới nhất của Rails, Ruby và Docker. Hi vọng bài viết sẽ phần nào hữu ích để bạn có thể tự config Docker cho project của mình.