Docker Compose và những kiến thức cơ bản
Docker Compose Cơ bản
Lời mở đầu
Như ở bài trước các bạn đã có thể chạy các container của riêng mình. Ví dụ như nếu bạn muốn chạy image backend của các bạn đã build ra thì các bạn dùng lệnh run image đó để tạo thành container, và nếu muốn chạy 1 container frontend nữa thì các bạn cũng sẽ làm tương tự như trên và nếu các bạn muốn chạy thêm cả image để lưu trữ dữ liệu như mysql thì các bạn lại phải run nó lên. Với cách thủ công như vừa rồi thì khá tốn sức, đôi khi có thể lỗi do thiếu mất biến môi trường hoặc quên ánh xạ cổng,... Vậy thì hôm nay mình và các bạn tìm hiểu về Docker Compose, các lý thuyết căn bản và các bài thực hành để giải quyết vấn đề trên nhé
Lý thuyết
Docker Compose là gì
- Docker Compose là một công cụ giúp bạn quản lý nhiều container cùng lúc trong Docker.
- Docker Compose cho phép bạn định nghĩa các container cần thiết để triển khai ứng dụng của bạn trong cùng 1 tệp cấu hình ( .yaml ).
- Các tệp cấu hình này chứa các thông tin về image, environment, network, volume ...
Các thành cở bản của Docker Compose
- Compose file: Đây là một tệp YAML ( docker-compose.yml ) chứa thông tin cần thiết về container để triển khai ứng dụng của bạn.
- Các dịch vụ (services): Là một nhóm container mà các bạn muốn tạo ra. Mỗi service đại diện cho một thành phần của ứng dụng, ví dụ như service backend, service frontend, service database...
- Container: Là một đối tượng chứa ứng dụng của bạn và các thành phần của nó, bao gồm phần mềm và cấu hình của ứng dụng. Docker Compose sử dụng các container để triển khai các services được định nghĩa trong tệp docker-compose.yml.
- Các mạng (networks): Định nghĩa các mạng ảo để các container trong cùng một service hoặc các service khác nhau liên lạc với nhau. Các mạng này cho phép các container truy cập lẫn nhau bằng tên hoặc địa chỉ IP.
- Các khối lưu trữ (volumes): Là một phần quan trọng của quá trình triển khai Docker Compose, cho phép bạn lưu trữ dữ liệu của ứng dụng của mình độc lập với các container. Điều này giúp cho việc sao lưu, phục hồi và quản lý dữ liệu trở nên dễ dàng hơn.
- Các biến môi trường (environment variables): Là các biến môi trường được sử dụng để cấu hình các container của dịch vụ. Điều này giúp bạn dễ dàng định nghĩa các cấu hình của ứng dụng của mình trong tệp docker-compose.yml một cách linh hoạt hơn.
- Command line interface (CLI): Là giao diện dòng lệnh để chạy các lệnh liên quan đến Docker Compose, cho phép tạo, triển khai, quản lý và xóa các container và service.
Thực hành
Cài đặt
Nếu các bạn đã cài Docker Desktop thì bạn không cần cài riêng lẻ, vì Docker Compose đã được bao gồm trong gói Docker Desktop ( Bạn đã cài ở bài trước ) rồi. Bạn có thể kiểm tra bằng cách mở command line hoặc powershell rồi gõ lệnh:
docker-compose --version
Nếu Docker Compose đã được cài đặt thành công, phiên bản của nó sẽ được hiển thị trên màn hình.
Nếu như máy bạn nào chưa cài Docker Compose thì các bạn hãy mở terminal lên rồi paste những lệnh bên dưới:
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
Rồi kiểm tra bằng lệnh sau:
docker-compose --version
Nếu hiển thị phiên bản của Docker Compose thì đã thành công.
Khởi tại và chạy docker-compose file đầu tiên
Viết docker-compose file đầu tiên
Bây giờ chúng ta cần triển khai 2 container:
- Container chạy Backend
- Container chạy Database ( MongoDB ).
Container Backend connect đến container chạy Database để lưu trữ dữ liệu. Trường hợp nếu không may container bị chết hoặc vô tình bị xoá đi thì khi khởi tạo lại 2 container này, dữ liệu trong database và các cấu hình vẫn giữ như lúc trước khi "die".
Mục đích của bài thực hành này là:
- Định nghĩa ra được các thành phần cần thiết để có thể khởi ra 2 container trên.
- Khởi tạo 2 container hoạt động trên cùng 1 network để nó có thể "giao tiếp" với nhau mà không phụ thuộc vào mạng bên ngoài máy host
- Khi xoá và khởi tạo lại các container thì dữ liệu cũ vẫn còn bằng cách chỉ định một thư mục hoặc một file trên máy host làm mount point cho volume của container. Nếu container "die" thì khởi tạo lại container mới mount vào thư mục này thì sẽ đảm bảo dữ liệu cũ không bị mất
Ở bài thực hành này, mình sẽ vẫn dùng code từ bài trước, và lần này dữ liệu người dùng sẽ lấy từ database ra chứ không phải trả về fix cứng như bài trước nữa ( các bạn checkout sang nhánh feature/docker-compose
để có thể thấy được code của bài thực hành này nhé, link github tại đây
Đây là file Docker compose mà mình sử dụng để giải quyết bài toán trên:
# Phiên bản của docker-compose file
version: '3.8'
# Định nghĩa 2 service là api cho backend và db cho database
services:
api:
# Service backend được build từ chính thư mục hiện tại qua Dockerfile
build:
context: .
dockerfile: Dockerfile
image: nestjs-basic-api
# Đặt tên cho container
container_name: nestjs-basic-api
# Container sẽ luôn được khởi động lại khi nó thoát
restart: always
# Ánh xạ cổng từ container ra máy host
ports:
- '${APP_PORT}:${APP_PORT}'
# Container này nằm trong mạng có tên app-network
networks:
- app-network
# Kết nối container này với container db
links:
- db:db
# Container này sẽ được khởi tạo sau khi container database khởi tạo xong
# Nếu không có thuộc tính này thì cả 2 container được khởi tạo cùng lúc
depends_on:
- db
# Các biến môi trường truyền vào trong container
environment:
MONGODB_URL: ${MONGODB_URL}
# Ánh xạ thư mục chứa code vào trong container để khi thay đổi code bên ngoài thì bên trong cũng thay đổi
volumes:
- .:/nest
db:
# Service này sử dụng image mongo để chạy
image: mongo
# Đặt tên cho container
container_name: nestjs-basic-db
restart: always
networks:
- app-network
ports:
- '${DB_PORT}:27017'
environment:
MONGO_INITDB_DATABASE: ${MONGO_INITDB_DATABASE}
MONGO_INITDB_ROOT_USERNAME: ${MONGO_INITDB_ROOT_USERNAME}
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD}
volumes:
- ./server:/data/db
# File mongo-init.js trên máy host được copy vào container với tên là mongo-init.js trong thư mục /docker-entrypoint-initdb.d và chỉ được đọc ( ro: read only ).
- ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
networks:
app-network:
# Mạng app-network sử dụng driver là bridge ( cho phép các container trong cùng network có thể "nói chuyện" được với nhau thông qua hostname )
driver: bridge
Đây là file không giải thích:
version: '3.8'
services:
api:
build:
context: .
dockerfile: Dockerfile
image: nestjs-basic-api
container_name: nestjs-basic-api
restart: always
ports:
- '${APP_PORT}:${APP_PORT}'
networks:
- app-network
links:
- db:db
depends_on:
- db
environment:
MONGODB_URL: ${MONGODB_URL}
volumes:
- .:/nest
db:
image: mongo
container_name: nestjs-basic-db
restart: always
networks:
- app-network
ports:
- '${DB_PORT}:27017'
environment:
MONGO_INITDB_DATABASE: ${MONGO_INITDB_DATABASE}
MONGO_INITDB_ROOT_USERNAME: ${MONGO_INITDB_ROOT_USERNAME}
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD}
volumes:
- ./server:/data/db
- ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
networks:
app-network:
driver: bridge
Đây là file mongo-init.js
:
db.createUser({
user: 'admin',
pwd: 'admin',
roles: [
{
role: 'readWrite',
db: 'nestjsdb',
},
],
});
Note:
- File docker-compose này định nghĩa hai service là api và db cùng thuộc mạng app-network.
- Service api sử dụng image
nestjs-basic-api
được build từ Dockerfile nằm trong thư mục hiện tại, container sẽ được đặt tên lànestjs-basic-api
. Service này cũng được liên kết với service db thông qua linkdb:db
và phụ thuộc vào servicedb
. Cuối cùng, service api mount thư mục hiện tại trên máy host vào thư mục/nest
trong container. - Service db sử dụng image
mongo
, đặt tên container lànestjs-basic-db.
Container này cũng được ánh xạ thư mục./server
trên máy host vào thư mục/data/db
trong container và file./mongo-init.js
trên máy host sẽ được copy vào container với tên làmongo-init.js
trong thư mục/docker-entrypoint-initdb.d
và chỉ được đọc ( ro: read only ). Cuối cùng, service db định nghĩa biến môi trường để khởi tạo MongoDB. - Thư mục
/data/db
là thư mục mặc định để lưu trữ dữ liệu trong container MongoDB, chúng ta cần ánh xạ thư mục này ra bên ngoài máy host vào thư mục./server
điều này có nghĩa khi containerdb
bị xoá đi hoặc bị khởi động lại thì dữ liệu cũ sẽ không bị mất. - File
mongo-init.js
để tạo một user mới với thông tin đăng nhập làadmin/admin
và được cấp quyền đọc và ghi cho databasenestjsdb
. - Biến
MONGODB_URL
được định nghĩa truyền vào containernestjs-basic-api
, biến này được định nghĩa trong file .env như sau:
mongodb://admin:admin@db:27017/nestjsdb
.
Như ở trên đã nói 2 container trên cùng 1 mạngdriver: bridge
, chúng ta đang sử dụng driver mạng mặc định là bridge. Điều này cho phép các container trong cùng một mạng ( ở đây làapp-network
có thể liên lạc được với nhau thông quahostname
) vì thế chúng ta chỉ cần viếtdb
ở biếnMONGODB_URL
ở trên mà không cần biết chính xác IP address của container chạy database.
Tiếp theo chúng ta sẽ chạy file docker-compose này bằng lệnh:
docker-compose up -d
Ở đây:
- up: Khởi động các container và các dịch vụ được định nghĩa trong file docker-compose.yml.
- -d: chạy ở chế độ nền ( detach ), nghĩa là không cần phải theo dõi đầu ra của các container trong terminal
Note:
- Nếu không có tham số
-d
thì trên terminal sẽ hiển thị logs của cả 2 conainer. Vì thế mình nghĩ để nó chạy nền là tốt nhất, nếu muốn xem logs trực tiếp của container nào thì chỉ cần chạydocker logs 52c515295260 -f
, ở đây:- logs: Xem các logs của container
- 5dade516ebf9: ContainerID cần xem
- -f: Trực tiếp theo dõi ( follow ) các logs của container, nếu không có tham số này thì nó chỉ hiện các logs hiện tại và khi có logs mới thì nó không hiển thị.
Và đây là kết quả sau khi chạy file docker-compose:
Như chúng ta đã thấy là docker đã khởi tạo ra 2 container với tên và sử dụng các image tương ứng mà chúng ta đã định nghĩa bên trong file docker-compose.yaml.
Chúng ta cùng kiểm tra xem 2 container trên có đang cùng 1 mạng hay không bằng cách sử dụng:
docker network ls
docker network inspect 43921733a76a
Sau khi container của chúng ta đã được khởi tạo, chúng ta sẽ test thử xem các APIs bên trong container có hoạt động đúng không nhé. Ở đây mình viết 2 APIs:
- API tạo mới user
/user
với phương thức làPOST
và body cần truyền vào có những thuộc tính như: username, password, email - API lấy toàn bộ user có trong database
Chúng ta bắt đầu test với API tạo mới user bằng cách mở terminal lên rồi gõ:
curl -X POST -d 'username=Grey&password=1234&email=grey@example.com' localhost:3000/user
Vậy là API tạo mới user của chúng ta đã hoạt động rồi đó, tiếp theo hãy thử tạo ra 1 và user mới rồi lấy toàn bộ user ra bằng lệnh:
curl localhost:3000/user
Vậy là 2 container của chúng ta đã hoạt động đúng rồi. Tiếp theo chúng ta sẽ xoá 2 container này là khởi tạo lại để xem dữ liệu cũ còn không nhé:
docker compose down
Đây là lệnh được sử dụng để dừng và xóa các container, mạng và bất kỳ tài nguyên khác được tạo bởi docker-compose.yml tương ứng mà không làm mất dữ liệu được lưu trong các volume đã được mount vào các container đó
Việc còn lại của các bạn là khởi tạo lại và kiểm tra xem các users mà các bạn đã tạo trước đó còn không nhé!
Viết Docker Compose file cho Frontend
Sau khi chúng ta đã khởi tạo thành công container cho Backend và Database thì tiếp theo chúng ta sẽ tạo ra thêm container Frontend để hiển thị toàn bộ users ra giao diện người dùng nhé, ở đây mình tách project cho Backend và Frontend riêng nhau nên cần viết Dockerfile và Docker Compose file riêng. Code phần frontend các bạn có thể tham khảo ở đây ( các bạn có thể dùng Dockerfile và docker-compose.yaml ở nhánh master
hoặc nhánh feature/nginx
đều được )
Frontend mình dùng Reactjs, và đây là file docker-compose.yaml:
version: '3.8'
services:
frontend:
container_name: reactjs-basic
build:
context: .
dockerfile: Dockerfile
restart: always
ports:
- 8080:80
Và đây là file Dockerfile:
FROM node:14-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
CMD [ "npm", "run", "start" ]
Ở phía frontend, mình chỉ cần call đến API lấy ra toàn bộ người dùng để hiển thị, các lệnh để chạy file docker-compose.yaml thì ở phía trên, các bạn hãy thực hành để quen tay nhé.
Và đây là kết quả sau khi chạy lên được frontend:
Các lưu ý
Khi sử dụng Docker Compose, có một số lưu ý quan trọng cần phải lưu ý để đảm bảo hệ thống hoạt động đúng cách và hiệu quả. Sau đây là một số điểm quan trọng cần lưu ý:
- Phiên bản của file docker-compose.yml ( version ): Phiên bản của Docker Compose được sử dụng cần phải được xác định để đảm bảo tính tương thích và đúng cú pháp khi sử dụng.
- Sử dụng volumes: Đường dẫn trên host và đường dẫn trong container phải đúng và tương ứng với nhau để đảm bảo dữ liệu được đồng bộ hóa đúng cách, và cần lưu ý về quyền truy cập vào thư mục khi sử dụng volumes.
- Sử dụng networks: Tên của network cần phải đúng và tương ứng với network được định nghĩa trong file docker-compose.yml để đảm bảo tính đúng đắn và đồng bộ giữa các container.
- Sử dụng image và build trong Dockerfile: Đường dẫn đến Dockerfile và tên của image cần phải đúng và tương ứng với nhau để đảm bảo quá trình build và tạo image được thực hiện đúng cách.
- Sử dụng environment variables: Các biến môi trường cần phải được định nghĩa đúng với các biến trong các file cấu hình để đảm bảo tính đúng đắn và đồng bộ giữa các container.
- Sử dụng các options như restart và depends_on: Các options này có thể ảnh hưởng đến quá trình khởi động và hoạt động của container, vì vậy cần phải cân nhắc ( như ở bài thực hành đầu tiên ).
Kết luận
Qua bài viết này, chúng ta đã đi qua lý thuyết và bài thực hành nho nhỏ để bước đầu tiếp cận với Docker Compose. Việc sử dụng Docker Compose giúp tối ưu quá trình triển khai ứng dụng, nó có thể triển khai nhiều container cùng 1 lúc mà vẫn đảm bảo tính nhất quán và đáng tin cậy của ứng dụng chính vì thế giúp chúng ta tiết kiệm thời gian và công sức, đồng thời cũng giảm thiểu các lỗi liên quan đến cấu hình và triển khai ứng dụng.
Chúng ta đã hoàn thành bài viết về Docker Compose, bài tiếp theo thì mình sẽ cùng với các bạn tìm hiểu tiếp về CI/CD để có cái nhìn đầu tiên và cơ bản nhất về cách triển khai tự động hoá việc build và deploy ứng dụng nhé. Cảm ơn các bạn đã xem đến cuối bài viết của mình 😄
All rights reserved