Sử dụng Docker để chạy ứng dụng NodeJS

Mục đích

Ban đầu khi blog này được triển khai trên server, mình đã cài đặt thủ công tất cả các thành phần cần thiết:

  • NodeJS
  • PM2
  • Redis
  • Nginx
  • Letsencrypt

Sau đó là đến phần chạy letsencrypt để lấy SSL key, cài đặt PM2 để chạy ứng dụng node, cấu hình nginx để kết nối web request đến ứng dụng node ...

Nói thì nhanh vậy nhưng để hoàn thành mất cả 2 ngày, phần vì mình vẫn còn non tay, nhưng k thể phủ nhận cài đặt một server từ đầu thủ công rất mất thời gian, lại nhiều lỗi vặt liên quan đến các gói phụ thuộc, phân quyền, blah blah.

Vì vậy mình quyết định dùng docker để đóng gói môi trường cho project này.

Một lý do nữa là sử dụng docker có thể dễ dàng cho việc tích hợp triển khai tự động, bằng cách dựng một docker image sau đó triển khai image này thay vì việc phải cập nhật code mới trên server.

Cấu hình Docker

Thành phần

Bằng cách nhìn vào các thành phần mà mình phải cài đặt trên server ở phần trên, chúng ta có thể nhận thấy docker cần phải có những services sau:

  • NodeJS server có cài đặt PM2 để chạy ứng dụng
  • Redis server sử dụng cho ứng dụng
  • Nginx server để làm web interface
  • Letsencrypt server để đăng ký SSL

Mình quyết định không cài đặt service Letsencrypt, bởi hiện tại trên server thật đã có sẵn SSL key, hơn nữa mỗi lần rebuild chúng ta lại request key SSL mới xem ra không có lợi cho Letsencrypt.

Như vậy hệ thống của chúng ta bao gồm 3 service.

Với redis và nginx chúng ta có thể sử dụng sẵn các image chính thức của nhà phát triển, như vậy chúng ta chỉ cần build ứng dụng nodejs mà thôi.

Dockerfile cho ứng dụng NodeJS

Ứng dụng NodeJS của chúng ta gồm 2 thành phần:

  • Server: là một expressjs app, source code nằm trong thư mục ./server
  • Frontend: là tập hợp các app frontend dưới dạng static và serve bởi expressjs, nằm trong thư mục ./app

Như vậy image của chúng ta cần phải có base là nodejs, ngoài ra cần phải được cài đặt PM2, may mắn là PM2 đã dựng sẵn một docker image của họ, chúng ta có thể sử dụng ngay.

Dockerfile để build ứng dụng của chúng ta như sau

# PM2 base image
FROM keymetrics/pm2:latest-alpine

# Change working directory
WORKDIR /midoriki

# Bundle APP files
COPY app app/
COPY server server/
COPY package.json .
COPY .env .
COPY ecosystem.config.js .

# Install app dependencies
ENV NPM_CONFIG_LOGLEVEL warn
RUN npm install --production

# Expose the listening port of your app
EXPOSE 8080

# Show current folder structure in logs
RUN ls -al -R

# PM2 start command
CMD [ "pm2-runtime", "start", "ecosystem.config.js" ]

Docker compose

Tiếp theo chúng ta sử dụng docker compose để định nghĩa các services cần thiết

File docker-compose.yml sẽ có nội dung như sau

version: '3'
services:
  redis:
    image: "redis:alpine"
    container_name: midoriki_redis
    networks:
      - app-network
  app:
    build:
      context: .
      dockerfile: docker/Dockerfile
    container_name: midoriki_app
    depends_on:
      - redis
    networks:
      - app-network
  web:
    image: "nginx"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./docker/nginx.conf:/etc/nginx/conf.d
      - /etc/letsencrypt:/etc/letsencrypt
    container_name: midoriki_web
    depends_on:
      - app
    networks:
      - app-network
networks:
  app-network:
    driver: bridge

Chúng ta đã định nghĩa 3 services theo như thiết kế bên trên, mở 2 cổng 80 và trên service nginx để giao tiếp với web request.

Điểm đặc biệt đó là chúng ta mount 2 volume vào service nginx:

  • /etc/letsencrypt: mount thư mục letsencrypt chứa SSL key vào nginx container
  • nginx.conf: file cấu hình cho nginx để làm cầu nối giữa web request và ứng dụng nodejs

Cấu hình nginx

server {
    server_name _;
    return 404;
}
server {
    listen 80;
    server_name midoriki.com www.midoriki.com;
    return https://$host$request_uri;
}
server {
    listen ssl http2;
    server_name midoriki.com www.midoriki.com;
    ssl_certificate /etc/nginx/ssl/midoriki.com/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/midoriki.com/privkey.pem;
    location / {
        proxy_pass http://app:8080;
        .
        . <other options here>
        .
    }
}

Cấu hình này thỏa mãn các tác vụ sau:

  • Trả về cho tất cả các request k trùng với server name bên dưới.
  • Chuyển hướng http sang https
  • Thiết lập SSL với public và secret key từ Letsencrypt
  • Nhận request https và chuyển sang cho ứng dụng nodejs - ở đây là phần
proxy_pass http://app:8080;

với app là tên service mà chúng ta đặt ứng dụng nodejs, định nghĩa ở file docker-compose.yml.

Triển khai

Sau khi đã hoàn thành các cài đặt, chúng ta có thể tiến hành deploy hệ thống.

Trước hết chúng ta cần cài đặt docker trên server

sudo apt-get update

sudo yum install -y yum-utils device-mapper-persistent-data lvm2

sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

sudo yum install docker-ce docker-ce-cli containerd.io

sudo systemctl start docker

# add current user into docker group, to bypass sudo when run docker command
sudo groupadd dockerdocker $USER

# enable docker on startup
sudo systemctl enable docker

# verify docker was installed successfully
docker info

Sau đó tiếp tục cài đặt docker-compose

sudo yum install -y python python-pip

pip install --user docker-compose

docker-compose --version

Tiếp theo, chúng ta clone source code từ github và khởi chạy docker-compose

cd <somewhere you want>

git clone git@github.com:midoriki/midoriki.git

cd midoriki

docker-compose up -d

# check if docker containers are running
docker ps

Như vậy chúng ta đã cài đặt thành công một hệ thống chạy trên docker.