+16

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 ...

dockercompose.png

Hình 1: Giới thiệu về Docker Compose

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:

  1. Container chạy Backend
  2. 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

image.png

Hình 2: Mô hình triển khai 2 container Backend và Database

Ở 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 link db:db và phụ thuộc vào service db. 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 container db 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 database nestjsdb.
  • Biến MONGODB_URL được định nghĩa truyền vào container nestjs-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ạng driver: 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 qua hostname ) vì thế chúng ta chỉ cần viết db ở biến MONGODB_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ạy docker 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:

image.png

Hình 3: Kết quả chạy file docker-compose.yaml

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

image.png

Hình 4: 2 container cùng chung 1 mạng

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:

  1. 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
  2. API lấy toàn bộ user có trong database

APIs.png

Hình 5: APIs tạo mới và lấy toàn bộ user 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

image.png

Hình 6: Kết quả API tạo mới 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

image.png

Hình 7: Kết quả API lấy toàn bộ 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" ]

code.png

Hình 8: Code phía frontend hiển thị ra toàn bộ users

Ở 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:

image.png

Hình 9: Kết quả hiển thị toàn bộ users

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 imagebuild 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ư restartdepends_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

Viblo
Let's register a Viblo Account to get more interesting posts.