1. Mở đầu
-
Xin chào các bạn, sau khi đi qua hai phần đầu của series:
Docker - Chưa biết gì đến biết dùng
, chúng ta đã tìm hiểuDocker, dockerfile, image, container ...
là gì và sử dụng chúng như thế nào. -
Hôm nay chúng ta sẽ tiếp tục tìm hiểu
docker-compose
- một công cụkết nối
cáccontainer
lại và làm cho chúng có thểtương tác
với nhau.
2. Bài toán
Câu chuyện đặt ra là:
-> Chúng ta muốn ứng dụng Docker cho
-
Dự án
mới
-
Hoặc dự án
đang phát triển
-> Vậy chúng ta làm như thế nào ?
-
Qua tìm hiểu ở hai phần đầu của series, chúng ta hoàn toàn có thể sử dụng
Dockerfile
-
Cài đặt tất cả những môi trường cần thiết (như mysql, redis, php,... ) lên một container
duy nhất
-
Rồi chạy project trên container đó.
-> Tuy nhiên:
-
Nếu như bạn muốn dùng kết hợp nhiều
image có sẵn
trên DockerHub thì sao ? -
Nếu một cơ sở dữ liệu
dùng chung
cho nhiều project thì sẽ xử lý thế nào ? -
Hơn nữa, với tư duy của OOP, 1 class thì không nên
cõng nhiều nhiệm vụ
.
-> Từ đó sinh ra docker-compose để kết nối các container riêng lẻ với nhau.
- Khi đó, chúng ta sẽ xây dựng nhiều container, khi nào cần tương tác với database thì gọi tới
container mysql
chẳng hạn, tương tác với redis thì gọi tớicontainer redis
, cần cái gì thì gọi tới container làm nhiệm vụ đó.
- Cùng nhìn hình ảnh con
bạch tuộc
đại diện chodocker-compose
(mình đoán thế) đang dùng các xúc tu để cuộn các container lên kìa.
-> Okie, ý tưởng cơ bản là vậy, cùng tìm hiểu chi tiết nhé !
-
À quên, bài viết sử dụng Rails framework (một framework của Ruby dùng để lập trình web) để ứng dụng Docker, còn với các framework của
PHP (Laravel, Yii, ...)
hayPython (Django, ...)
thì mình sẽ tìm cách làm tương tự nha. -
Quan trọng là mình hiểu cách sử dụng
docker-compose
, nếu có điều kiện, mình sẽ tìm hiểu và viết thêm bài hướng dẫn sử dụng Docker trên Laravel hoặc các framework khác. -
Các bạn cũng có thể skip để chuyển sang Docker: Chưa biết gì đến biết dùng ( Phần 4 - Một số trick tối ưu và lưu ý )
Let go
3. Cài đặt Docker-compose
Tham khảo trên trang chủ nào, trên Linux
thì sẽ như sau:
Step 1:
Run this command to download the latest version of Docker Compose.
sudo curl -L "https://github.com/docker/compose/releases/download/1.22.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
Step 2:
Apply executable permissions to the binary
sudo chmod +x /usr/local/bin/docker-compose
Step 3:
Test the installation
docker-compose --version
-> Đây là phiên bản đang cài đặt trên máy tính của mình:
docker-compose version 1.23.1, build b02f1306
-> Xong roài, có vẻ nhanh - gọn - nhẹ
4. Cấu trúc thư mục
-> Tham khảo tài liệu từ đâu ?
-
Khá là hay ho khi trên trang chủ Docker đã có bài hướng dẫn Docker for Rails.
-
Thông thường, mình thấy tham khảo trên trang chủ cũng khá là đủ rồi
-
Nhưng cũng hơi bất ngờ khi trong thực tế làm dự án, anh em trong team nhận thấy vẫn còn một số điểm
chưa được thực sự tối ưu
-
Nên sau đây chúng ta cùng tìm hiểu cách
apply docker-compose
dựa vào bài hướng dẫn trên và đống thờitối ưu
hơn nhé ! Nếu các bạn nhận thấy phần nào có thể làm tốt hơn thì mời comment bên dưới nha.
-> Tạo các file cấu hình
Bao gồm 3 file sau:
-
1. docker / entrypoint.sh
:Liệt kê những câu lệnh cần chạy sau khi bật container.
-
2. Dockerfile
Về công dụng của nó thì chắc hẳn các bạn còn nhớ chúng ta đã đề cập ở bài trước:
-
3. docker-compose.yml
:Dùng để
khai báo
vàđiều phối
hoạt động của cáccontainer
trong project.
5. Xác định các container cần thiết
Ở mức cơ bản nhất, chúng ta sẽ xây dựng 2 container:
-> Thứ nhất là container dùng để kết nối tới cơ sở dữ liệu.
-
Hiện nay, phần lớn các dự án có lẽ hay sử dụng
mysql
, để gần gũi và quen thuộc với mọi người nên mình sẽ dùng cơ sở dữ liệu mysql. -
Còn nếu hịn hò hơn thì bạn có thể tìm hiểu và sử dụng
postgresql
nhé -
Teachnical leader của dự án mình cũng khuyến khích sài công cụ này, và google một hồi thì có vẻ như nó có nhiều ưu điểm hơn thật, ví dụ như lưu trữ với kiểu dữ liệu array hay gán giá trị mặc định với kiểu dữ liệu
text
... -
Quay trở lại vấn đề chính nào, quan sát:
https://hub.docker.com/_/mysql/
Chúng ta sẽ thấy tên organization là
/_/
và bên dưới tên image có dòng chữDocker Official Images
Đây chính là những
image chính thức
được Docker cung cấp, chúng ta sẽ yên tâm hơn khi sử dụng, và khuyến cáo luôn là nên sử dụng (Hàng chính hãng, ít có khả năng gặp bug hơn hoặc chèn mã độc)
Trên DockerHub còn rất nhiều image khác dành cho mysql, ví dụ bitnami/mysql
Đây là những image do các cá nhân, tổ chức khác xây dựng, có thể sẽ có những cải tiến so với bản official nhưng độ tin cậy và chính xác thì khó mà bằng bản chuẩn được.
-> Thứ hai là container dùng cho web application.
-
Có thể có bạn chưa rõ, ta có PHP là một ngôn ngữ lập trình, Laravel là một PHP framework, dùng để lập trình web.
-
Thì tương tự như vậy, Ruby cũng là một ngôn ngữ lập trình, Rails là một Ruby framework dùng để lập trình web.
-
Lát nữa chúng ta dùng image ruby này để tạo
container
choweb application
nhé !
-> Okie xong, đã xác định được 2 image tương ứng với 2 container cần xây dựng.
6. Viết docker-compose
Chúng ta sẽ viết docker-compose.yml
trước để có cái nhìn tổng quan về các services trong project nhé
- Phiên bản của
docker-compose
version: '3.5'
- Liệt kê các services
services: mysql: ... ... ... app: ... ... ...
- Cài đặt cho từng services
mysql: image: mysql:5.7 container_name: mysql restart: always environment: MYSQL_ROOT_PASSWORD: root volumes: - docker/database:/var/lib/mysql
☑ image: Chỉ định image
để khởi động container
, ở đây ta dùng image có sẵn như đã nói ở mục 4.
☑ container_name: Chỉ định tên container tùy chỉnh, thay vì tên mặc định.
☑ restart: Giá trị mặc định là no
, còn nếu bạn đặt là always
thì container sẽ khởi động lại nếu mã thoát cho biết lỗi không thành công.
☑ environment: Thêm các biến môi trường
☑ volumes: Chia sẻ dữ liệu giữa container (máy ảo) và host (máy thật) hoặc giữa các container với nhau.
Ví dụ:
- Khi
container mysql
tạo và lưu dữ liệu thì dữ liệu này sẽ lưu ở trong thư mụcvar/lib/mysql
của container. Như vậy nếu như container này bị xóa đi thì chúng ta sẽmất toàn bộ data
-
Ôi buồn quá -_- Làm gì bây giờ ?
-
Giải pháp là chúng ta sẽ
sao lưu
dữ liệu đó ra ngoài máy host, như vậy khi container bị xóa, dữ liệu sẽ vẫn được lưu trữ ở máy host. Và ở khi bật lại container, dữ liệu lại được mount từ máy host vào trong container và chúng ta tiếp tục sử dụng nó bình thường. -
Thư mục lưu trữ data ở ngoài máy host sẽ không được
commit
vào git, ta đưa nó vào gitignore.# Ignore data backup docker/database
-
Ở một số hướng dẫn có lưu dữ liệu backup vào trong thư mục mà framework đã
gitignore
sẵn, ví dụ như đối vớiRails
là/tmp
hoặc lưu ở ngoài thư mục dự án thì không cầngitignore
nó nữa.
app: container_name: app build: . volumes: - .:/my_app ports: - "3000:3000" environment: DATABASE_HOST: mysql DATABASE_USER_NAME: root DATABASE_PASSWORD: root
☑ build: Sử dụng khi chúng ta không xây dựng container
từ image
có sẵn nữa mà xây dựng nó từ Dockerfile
.
- Nếu
Dockerfile
nằm cùng thư mục vớidocker-compose.yml
thì chỉ cần
build: .
- Nếu bạn muốn đặt
Dockerfile
trong thư mụcdocker
để cùng vớientrypoint.sh
cho gọn thì sửa thành
build: context: ./ dockerfile: docker/Dockerfile
☑ ports: Cấu hình cổng kết nối
Có thể chỉ định cả 2 cổng (HOST:CONTAINER) tức là (cổng ở máy thật: cổng ở máy ảo) hoặc chỉ định mình cổng cho máy ảo thôi.
Ví dụ: "2222:3333"
Khi bạn truy cập vào cổng 2222
ở máy thật thì sẽ được trỏ tới truy cập ở cổng 3333
của máy ảo.
☑ environment: Bổ sung các biến môi trường.
Lưu ý rằng DATABASE_HOST
chính là tên của service mysql.
Mình đã push code lên Github.
7. Viết Dockerfile
-> Bây giờ ta sẽ viết Dockerfile
cho container app
trên kia nhé, chỗ mà build .
ấy.
-
Image cơ sở
FROM ruby:2.5.1
-
Đánh dấu lãnh thổ chút
LABEL author.name="HoanKy" \ author.email="_@.com"
-
Cặt đặt các phần mềm cần thiết cho máy ảo
RUN apt-get update && \ apt-get install -y nodejs nano vim
-
Set timezone cho máy ảo (Optional)
ENV TZ=Asia/Ho_Chi_Minh RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
-
Chỉ định thư mục làm việc mặc định (Optional)
ENV APP_PATH /my_app WORKDIR $APP_PATH
-
Cài đặt framework cần thiết cho dự án
COPY Gemfile Gemfile.lock $APP_PATH/ RUN bundle install --without production --retry 2 \ --jobs `expr $(cat /proc/cpuinfo | grep -c "cpu cores") - 1`
-
Copy tất cả dữ liệu tự máy host vào trong container
COPY . $APP_PATH
-
Cấu hình file
entrypoint.sh
COPY docker/entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT ["entrypoint.sh"] EXPOSE 3000
-
Thiết lập câu lệnh mặc định sẽ chạy khi khởi động
container
CMD ["rails", "server", "-b", "0.0.0.0"]
-> Mình có push code mẫu lên Github
8. Viết entrypoint.sh
-> Chạy file sh
với bash chứ ko phải là sh hay zsh
#!/bin/bash
set -e
-> Xóa tiến trình cũ
rm -f /my_app/tmp/pids/server.pid
-
Mỗi khi khởi chạy
rails server
sẽ có một mãid
được sinh ra và lưu vào filetmp/pids/server.pid
để đánh dấu rằng đã tồn tại tiến trình rails đang chạy. -
Khi bạn stop
rails server
thì rails sẽ xóa nội dung file này, tuy nhiên trong trường hợp bạnkill rails process
thì rails server sẽ bị stop mà chưa kịp xóa nội dung file, và khi bạn start lại server thì sẽ gặp lỗi=> Booting Puma => Rails 5.0.0 application starting in development on http://0.0.0.0:3000 => Run `rails server -h` for more startup options A server is already running. Check /balabala/tmp/pids/server.pid. Exiting
-
Như vậy, ta sẽ xóa thủ công nó luôn để đảm bảo không gặp lỗi này, gọi là chặn từ lúc trứng nước.
-> Thực thi câu lệnh truyền vào.
exec "_@.com"
Mình có push code mẫu lên Github
9. Sử dụng docker-compose
9.1 Trường hợp tạo dự án mới
-> Khi setup project với Rails trực tiếp trên máy thật theo cách truyền thống
- Chạy câu lệnh sau để initialize project, khi đó các file, folder sẽ tự động được sinh ra:
rails new . --force --no-deps --database=mysql
-> Khi setup project thông qua Docker thì sao ?
-
Khi đó, chúng ta sẽ không cài Rails vào máy thật nữa.
-
Hãy run một
container
đã càiRails
lên và chạy câu lệnh trên bên trongcontainer
đó, rồi mount các file, folder của framework được vừa được tạo ra ngoài máy host. -
Ở đây, container đó chính là container
app
mà ta vừa xây dựng ở trên. -
Cú pháp để chạy một câu lệnh bên trong
container
như sau:sudo docker-compose run + tên container + Câu lệnh muốn chạy
Như vậy, bạn hãy chạy:
docker-compose run app rails new . --force --no-deps --database=mysql
- Việc này sẽ mấy chút thời gian, trong quá trình chờ đợi thì bạn nên theo dõi log ở Terminal để xem các bước Docker thực hiện như thế nào nhé !
-> Note 1: Khai báo Rails version
-
Nếu bạn cài Rails trực tiếp trên máy thật thì đã có sẵn phiên bản của Ruby (ngôn ngữ lập trình) và Rails (framework của ngôn ngữ) rồi, ví dụ:
$ ruby -v ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux] $ rails -v Rails 5.2.2.1
Còn như ở Dockerfile phía trên, chúng ta mới chỉ có
FROM ruby:2.5.1
chứ chưa cài đặt Rails.
-
Thứ hai là khi build image, ở step
COPY Gemfile Gemfile.lock $APP_PATH/
có nghĩa là copy Gemfile từ máy host vào container thì chúng ta chưa có file này ở máy host:
Như thế sẽ gặp lỗi:
no such file or directory
-
Vậy nên, chúng ta tạo thêm 2 file: Gemfile và Gemfile.lock
Gemfile:
source "https://rubygems.org" gem "rails", "~>5"
Gemfile.lock
# File này để trống
-
Rồi sau đó hẵng chạy lại câu lệnh
docker-compose run app rails new . --force --no-deps --database=mysql
-
Mình đã push code mẫu lên Github rồi nhé, bởi vì đây là config thư viện của Rails nên không giới thiệu chung với các config của Docker.
-> Note 2: Cấp lại quyền cho file, folder
- Có thể bạn đã biết, mặc định thì docker chạy với
user root
nên những file, folder nó tạo ra (sau khimount
từcontainer
ra máyhost
) cũng ở quyềnroot
.
-
Vậy nên khi bạn dùng editor để
chỉnh sửa
những file này thìLinux OS
sẽ thông báo.Permission denied
-
Giải pháp thì set lại quyền cho nó thôi
sudo chown -R $USER:$USER .
-> Note 3: Cấu hình để kết nối tới database
- Đối với
Rails framework
thì config này nằm ở fileconfig/database.yml
Sửa từ
default: &default adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: host: localhost
thành
default: &default adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: <%= ENV.fetch("DATABASE_USER_NAME") || "root" %> password: <%= ENV.fetch("DATABASE_PASSWORD") || "root" %> host: <%= ENV.fetch("DATABASE_HOST") || "mysql" %>
- Xong xuôi thì bạn chuyển qua mục 10.2 được rồi.
9.2 Trường hợp cho dự án đang phát triển
-> Sửa cấu hình kết nối database
- Chỉnh sửa file
config setting connect database
cho phù hợp nếu cần (ví dụ với Rails: config/database.yml )
-> Build các image cần thiết
-
Cú pháp
docker-compose build
-
Nếu gặp lỗi này
IOError: Can not read file in context: /home/nguyenvanhoan/GitRepo/docker_tutorial/docker/database/ca-key.pem
Do một số file trong thư mục
back_up database
cần quyềnroot
để truy cập, hãy đổi thànhsudo docker-compose build
-> Khởi chạy container:
-
Cú pháp
sudo docker-compose up
Màn hình log:
Òa, log bắn ra ầm ầm, kệ nó đi, bạn vào địa chỉ :
http://localhost:3000
và tận hưởng thành quả nào:
-> Vâng, bug đỏ lòm đập vào mặt, lỗi này là do chúng ta chưa tạo database.
-
Tạo cơ sở dữ liệu
Với mỗi framework sẽ có cách khác nhau để tạo
database
.Còn đối với rails, nếu ở máy thật, bạn cần gõ 2 câu lệnh sau để tạo database:
# Xóa và tạo lại các bảng và quan hệ giữa chúng rails db:migrate:reset
Còn bây giờ, chúng ta hãy truy cập vào máy ảo để chạy câu lệnh này, bằng cách
sudo docker-compose run + tên container + Câu lệnh muốn chạy
Và ở đây, ta cần:
sudo docker-compose run app rails db:migrate:reset
-
Truy cập vào
http://localhost:3000
nào -
Lưu ý
- Nếu vừa rồi bạn ấn
Ctrl + C
để dừngdocker-compose up
thì khi chạy câu lệnh tạo cơ sở dữ liệu sẽ gặp lỗi
Mysql2::Error::ConnectionError: Unknown MySQL server host 'mysql' (-2)
- Nếu vừa rồi bạn ấn
-
Vì sao ?
Nhìn vào log trên ta sẽ thấy
Tức là service mysql
đã bị dừng lại, vậy nên service app
của chúng ta không thể kết nối tới mysql service
.
- Để giải quyết điều này thì có nhiều cách:
-
Đơn giản nhất là bạn giữ nguyên tab đang chạy
docker-compose up
và mở thêm tab để chạy migrate data. -
Dùng option
-d
docker-compose up -d
-
Thay đổi cách khởi động các
container
thủ côngBài cũng đã dài, chúng ta sẽ tìm hiểu cách 3 ở phần 4 nhé, đây cũng là cách khuyến khích sử dụng.
-
Thay đổi cách khởi động các
container
bằngdepends_on
Cách này không khuyến khích (Cũng sẽ giải thích ở phần 4 luôn)
- Cũng đủ kiến thức cơ bản về
docker-compose
rồi - Nếu bạn gặp lỗi hoặc bị vướng ở đoạn nào chưa hiểu thì hãy để lại bình luận bên dưới, mình sẽ support nếu có thể nhé !
- Code mẫu cho phần core config mình đã push lên đây.
- Còn code sau khi các file được sinh ra thì mình đã push lên đây.
10. Updating ...
-
Còn một chút lưu ý cũng như một số mẹo để sử dụng docker nhanh, gọn nhẹ hơn.
=> Mình sẽ cập nhật ở phần 4 nhé. Mời cả nhà đón đọc !
-
###############################################
Thank for your attention
Mình đã viết xong Docker: Chưa biết gì đến biết dùng ( Phần 4 - Một số trick tối ưu và lưu ý ), mời các bạn đọc tiếp.