---
title: "Cách bảo mật ứng dụng Node.js được đóng gói bằng Nginx, Let's Encrypt và Docker Compose"
description: "Cách bảo mật ứng dụng Node.js được đóng gói bằng Nginx, Let's Encrypt và Docker Compose"
url: https://cloudfly.vn/techblog/cach-bao-mat-ung-dung-nodejs-duoc-dong-goi-bang-nginx-lets-encrypt-va-docker-compose
image: https://media.cloudfly.vn/posts/techblog_uAMZKNv.jpg
---

# Cách bảo mật ứng dụng Node.js được đóng gói bằng Nginx, Let's Encrypt và Docker Compose

## Giới thiệu

Có nhiều cách để nâng cao tính linh hoạt và bảo mật cho ứng dụng Node.js của bạn. Sử dụng proxy ngược như Nginx sẽ cung cấp cho bạn các yêu cầu cân bằng tải, lưu trữ nội dung tĩnh và triển khai *Transport Layer Security* (TLS). Việc bật HTTPS được mã hóa trên máy chủ của bạn sẽ đảm bảo rằng hoạt động liên lạc đến và đi từ ứng dụng của bạn vẫn được an toàn.

Việc triển khai proxy ngược bằng TLS/SSL trên vùng chứa bao gồm một bộ quy trình khác với hoạt động trực tiếp trên hệ điều hành máy chủ. Ví dụ: nếu bạn đang lấy chứng chỉ từ Let's Encrypt cho một ứng dụng chạy trên máy chủ, bạn sẽ cài đặt phần mềm cần thiết trực tiếp trên máy chủ của mình. Vùng chứa cho phép bạn thực hiện một cách tiếp cận khác. Khi sử dụng Docker Compose, bạn có thể tạo các vùng chứa cho ứng dụng, máy chủ web và ứng dụng khách Certbot để cho phép bạn lấy chứng chỉ của mình. Bằng cách làm theo các bước này, bạn có thể tận dụng tính mô-đun và tính di động của quy trình làm việc được đóng gói.

Trong hướng dẫn này, bạn sẽ triển khai ứng dụng Node.js với proxy ngược Nginx bằng Docker Compose. Bạn sẽ nhận được chứng chỉ TLS/SSL cho miền được liên kết với ứng dụng của bạn và đảm bảo rằng nó nhận được xếp hạng bảo mật cao từ SSL Labs. Cuối cùng, bạn sẽ thiết lập một công việc `cron` để gia hạn chứng chỉ của mình và giúp miền của bạn luôn được bảo mật.

## Điều kiện tiên quyết

Để làm theo hướng dẫn này, bạn sẽ cần:

* Máy chủ Ubuntu 18.04, non-**root** user có đặc quyền `sudo` và tường lửa đang hoạt động. Để được hướng dẫn về cách thiết lập những thứ này, vui lòng đọc hướng dẫn Thiết lập máy chủ ban đầu với Ubuntu 18.04.
* Docker và Docker Compose được cài đặt trên máy chủ của bạn. Để được hướng dẫn cài đặt Docker, hãy làm theo **Bước 1 và 2** của Cách cài đặt và sử dụng Docker trên Ubuntu 18.04. Để được hướng dẫn cài đặt Compose, hãy làm theo **Bước 1** của Cách cài đặt Docker Compose trên Ubuntu 18.04.
* Một tên miền đã đăng ký. Hướng dẫn này sẽ sử dụng **your_domain** xuyên suốt. Bạn có thể đăng ký tên miền tại công ty mà bạn chọn, chẳng hạn như CloudFly.
* Cả hai bản ghi DNS sau đây đều được thiết lập cho máy chủ của bạn. 
  - Bản ghi A có `<^>your_domain<^>` trỏ đến địa chỉ IP công cộng của máy chủ của bạn.
  - Bản ghi A có `www.<^>your_domain<^>` trỏ đến địa chỉ IP công cộng của máy chủ của bạn.

Khi bạn đã thiết lập xong mọi thứ, bạn đã sẵn sàng bắt đầu bước đầu tiên.

## Bước 1 - Nhân bản và kiểm tra ứng dụng node

Bước đầu tiên, bạn sẽ sao chép kho lưu trữ bằng mã ứng dụng Node, bao gồm Dockerfile để xây dựng hình ảnh ứng dụng của bạn bằng Compose. Sau đó, bạn sẽ kiểm tra ứng dụng bằng cách xây dựng và chạy nó bằng lệnh `docker run` mà không cần proxy ngược hoặc SSL.

Trong thư mục chính của non-**root** user của bạn, hãy sao chép kho lưu trữ `nodejs-image-demo` từ tài khoản GitHub. Kho lưu trữ này bao gồm mã từ quá trình thiết lập được mô tả trong Cách xây dựng ứng dụng Node.js bằng Docker.

Sao chép kho lưu trữ vào một thư mục. Ví dụ này sử dụng `node_project` làm tên thư mục. Hãy đặt tên thư mục này theo ý thích của bạn:
```command
git clone https://github.com/do-community/nodejs-image-demo.git <^>node_project<^>
```

Thay đổi vào thư mục `node_project`:
```command
cd node_project
```

Trong thư mục này, có một Dockerfile chứa hướng dẫn xây dựng ứng dụng Node bằng cách sử dụng hình ảnh Docker `node:10` và nội dung của thư mục dự án hiện tại của bạn. Bạn có thể xem trước nội dung của Dockerfile bằng cách sau:
```command
cat Dockerfile
```

```
[secondary_label Output]
FROM node:10-alpine

RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app

WORKDIR /home/node/app

COPY package*.json ./

USER node

RUN npm install

COPY --chown=node:node . .

EXPOSE 8080

CMD [ "node", "app.js" ]
```

Các hướng dẫn này xây dựng hình ảnh Node bằng cách sao chép mã dự án từ thư mục hiện tại vào vùng chứa và cài đặt các phần phụ thuộc bằng `npm install`. Họ cũng tận dụng bộ nhớ đệm và phân lớp hình ảnh của Docker bằng cách tách bản sao của `pack.json` và `package-lock.json`, chứa các phần phụ thuộc được liệt kê của dự án, từ bản sao của phần còn lại của mã ứng dụng. Cuối cùng, hướng dẫn chỉ định rằng vùng chứa sẽ được chạy với tư cách là non-**root** **node** user với các quyền thích hợp được đặt trên mã ứng dụng và thư mục `node_modules`.

Để biết thêm thông tin về các phương pháp hay nhất về Dockerfile và hình ảnh Node này, vui lòng khám phá phần thảo luận đầy đủ trong **Bước 3** của Cách xây dựng ứng dụng Node.js bằng Docker.

Để kiểm tra ứng dụng không có SSL, bạn có thể xây dựng và gắn thẻ hình ảnh bằng cách sử dụng `docker build` và cờ `-t`. Ví dụ này đặt tên cho hình ảnh `node-demo`, nhưng bạn có thể tự do đặt tên cho nó bằng tên khác:
```command
docker build -t node-demo .
```

Khi quá trình xây dựng hoàn tất, bạn có thể liệt kê hình ảnh của mình bằng `docker images`:
```command
docker images
```

Đầu ra sau đây xác nhận việc xây dựng hình ảnh ứng dụng:
```
[secondary_label Output]
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
node-demo           latest              23961524051d        7 seconds ago       73MB
node                10-alpine           8a752d5af4ce        3 weeks ago         70.7MB
```

Tiếp theo, tạo vùng chứa bằng `docker run`. Ba cờ được bao gồm trong lệnh này:

* `-p`: Điều này xuất bản cổng trên vùng chứa và ánh xạ nó tới một cổng trên máy chủ của bạn. Bạn sẽ sử dụng cổng `80` trên máy chủ trong ví dụ này, nhưng vui lòng sửa đổi cổng này nếu cần nếu bạn có một quy trình khác đang chạy trên cổng đó. Để biết thêm thông tin về cách thức hoạt động của tính năng này, hãy xem lại cuộc thảo luận này trong tài liệu Docker về liên kết cổng.
* `-d`: Cái này chạy container ở chế độ nền.
* `--name`: Điều này cho phép bạn đặt cho vùng chứa một cái tên dễ nhớ.

Chạy lệnh sau để xây dựng vùng chứa:
```command
docker run --name <^>node-demo<^> -p 80:8080 -d <^>node-demo<^>
```

Kiểm tra các container đang chạy của bạn bằng `docker ps`:
```command
docker ps
```

Đầu ra sau đây xác nhận rằng vùng chứa ứng dụng của bạn đang chạy:
```
[secondary_label Output]
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                  NAMES
4133b72391da        node-demo           "node app.js"       17 seconds ago      Up 16 seconds       0.0.0.0:80->8080/tcp   node-demo
```

Bây giờ bạn có thể truy cập miền của mình để kiểm tra thiết lập của mình: `http://<^>your_domain<^>`. Hãy nhớ thay thế `<^>your_domain<^>` bằng tên miền của riêng bạn. Ứng dụng của bạn sẽ hiển thị trang đích sau:

![description image](https://media.cloudfly.vn/posts/landing_page_McanSPI.png)

Bây giờ bạn đã kiểm tra ứng dụng, bạn có thể dừng vùng chứa và xóa hình ảnh. Sử dụng `docker ps` để lấy `CONTAINER ID` của bạn:
```command
docker ps
```

```
[secondary_label Output]
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                  NAMES
<^>4133b72391da<^>        node-demo           "node app.js"       17 seconds ago      Up 16 seconds       0.0.0.0:80->8080/tcp   node-demo
```

Dừng vùng chứa bằng `docker stop`. Đảm bảo thay thế `CONTAINER ID` được liệt kê ở đây bằng `CONTAINER ID` ứng dụng của riêng bạn:
```command
docker stop <^>4133b72391da<^>
```

Bây giờ bạn có thể xóa vùng chứa đã dừng và tất cả các hình ảnh, bao gồm cả hình ảnh không được sử dụng và hình ảnh lơ lửng (dangling images), bằng `docker system prune` và cờ `-a`:
```command
docker system prune -a
```

Nhấn `y` khi được nhắc ở đầu ra để xác nhận rằng bạn muốn xóa vùng chứa và hình ảnh đã dừng. Xin lưu ý rằng thao tác này cũng sẽ xóa bộ nhớ đệm bản dựng của bạn.

Sau khi thử nghiệm hình ảnh ứng dụng, bạn có thể chuyển sang xây dựng phần còn lại của thiết lập bằng Docker Compose.

## Bước 2 - Xác định cấu hình máy chủ web

Với ứng dụng Dockerfile của chúng ta đã có sẵn, bạn sẽ tạo một tệp cấu hình để chạy vùng chứa Nginx của mình. Bạn có thể bắt đầu với cấu hình tối thiểu bao gồm tên miền, gốc tài liệu, thông tin proxy và khối vị trí để chuyển các yêu cầu của Certbot tới thư mục `.well-known`, nơi nó sẽ đặt một tệp tạm thời để xác thực DNS cho của bạn tên miền phân giải đến máy chủ của bạn.

Đầu tiên, tạo một thư mục trong thư mục dự án hiện tại, `node_project`, cho tệp cấu hình:
```command
mkdir nginx-conf
```

Tạo và mở tệp bằng `nano` hoặc trình chỉnh sửa yêu thích của bạn:
```command
nano nginx-conf/nginx.conf
```

Thêm khối máy chủ sau vào các yêu cầu của proxy user vào vùng chứa ứng dụng Node của bạn và chuyển các yêu cầu của Certbot tới thư mục `.well-known`. Đảm bảo thay thế `<^>your_domain<^>` bằng tên miền của riêng bạn:
```nginx
[label ~/node_project/nginx-conf/nginx.conf]
server {
        listen 80;
        listen [::]:80;

        root /var/www/html;
        index index.html index.htm index.nginx-debian.html;

        server_name <^>your_domain<^> www.<^>your_domain<^>;

        location / {
                proxy_pass http://nodejs:8080;
        }

        location ~ /.well-known/acme-challenge {
                allow all;
                root /var/www/html;
        }
}
```

Khối máy chủ này sẽ cho phép bạn khởi động vùng chứa Nginx làm proxy ngược, nó sẽ chuyển các yêu cầu đến vùng chứa ứng dụng Node của bạn. Nó cũng sẽ cho phép bạn sử dụng plugin webroot của Certbot để lấy chứng chỉ cho miền của bạn. Plugin này phụ thuộc vào phương thức xác thực HTTP-01, sử dụng yêu cầu HTTP để chứng minh rằng Certbot có thể truy cập tài nguyên từ máy chủ phản hồi một tên miền nhất định.

Sau khi chỉnh sửa xong, hãy lưu và đóng tệp. Nếu bạn đã sử dụng `nano`, bạn có thể thực hiện việc này bằng cách nhấn `CTRL + X`, sau đó là `Y` và `ENTER`. Để tìm hiểu thêm về máy chủ Nginx và thuật toán khối vị trí, vui lòng tham khảo bài viết  về Tìm hiểu thuật toán lựa chọn khối vị trí và máy chủ Nginx.

Với các chi tiết cấu hình máy chủ web đã có, bạn có thể chuyển sang tạo tệp `docker-compose.yml`, tệp này sẽ cho phép bạn tạo các dịch vụ ứng dụng và vùng chứa Certbot mà bạn sẽ sử dụng để lấy chứng chỉ của mình.

## Bước 3 - Tạo tệp Docker Compose

Tệp `docker-compose.yml` sẽ xác định các dịch vụ của bạn, bao gồm ứng dụng Node và máy chủ web. Nó sẽ chỉ định các chi tiết như khối lượng (volumes) được đặt tên, điều này sẽ rất quan trọng để chia sẻ thông tin xác thực SSL giữa các vùng chứa, cũng như thông tin mạng và cổng. Nó cũng sẽ cho phép bạn chỉ định các lệnh sẽ chạy khi vùng chứa của bạn được tạo. Tệp này là tài nguyên trung tâm sẽ xác định cách các dịch vụ của bạn sẽ hoạt động cùng nhau.

Tạo và mở tệp trong thư mục hiện tại của bạn:
```command
nano docker-compose.yml
```

Đầu tiên, xác định dịch vụ ứng dụng:
```nginx
[label ~/node_project/docker-compose.yml]
version: '3'

services:
  nodejs:
    build:
      context: .
      dockerfile: Dockerfile
    image: nodejs
    container_name: nodejs
    restart: unless-stopped
```

Định nghĩa dịch vụ của `nodejs` bao gồm:

* `build`: Phần này xác định các tùy chọn cấu hình, bao gồm `context` và `dockerfile`, sẽ được áp dụng khi Compose xây dựng hình ảnh ứng dụng. Nếu muốn sử dụng hình ảnh hiện có từ sổ đăng ký như Docker Hub, bạn có thể sử dụng hướng dẫn `image` thay thế, kèm theo thông tin về tên người dùng, kho lưu trữ và thẻ hình ảnh của bạn.
* `context`: Điều này xác định bối cảnh xây dựng cho hình ảnh ứng dụng build. Trong trường hợp này, đó là thư mục dự án hiện tại được biểu thị bằng phần mở rộng `.`.
* `dockerfile`: Điều này chỉ định Dockerfile mà Compose sẽ sử dụng cho bản dựng - Dockerfile đã được xem xét ở Bước 1.
* `image`, `container_name`: Những tên này áp dụng cho hình ảnh và vùng chứa.
* `restart`: Điều này xác định chính sách khởi động lại. Mặc định là `no`, nhưng trong ví dụ này, vùng chứa được đặt để khởi động lại trừ khi nó bị dừng.

Lưu ý rằng bạn không bao gồm các liên kết gắn kết với dịch vụ này vì thiết lập của bạn tập trung vào việc triển khai hơn là phát triển. Để biết thêm thông tin, vui lòng đọc tài liệu Docker về bind mounts và volumes.

Để cho phép liên lạc giữa ứng dụng và vùng chứa máy chủ web, hãy thêm bridge network có tên là `app-network` sau định nghĩa khởi động lại:
```nginx
[label ~/node_project/docker-compose.yml]
services:
  nodejs:
...
    <^>networks:<^>
      <^>- app-network<^>
```

Bridge network do người dùng xác định như thế này cho phép liên lạc giữa các vùng chứa trên cùng một máy chủ Docker daemon. Điều này hợp lý hóa lưu lượng truy cập và liên lạc trong ứng dụng của bạn, vì nó mở tất cả các cổng giữa các vùng chứa trên cùng một bridge network, trong khi không để lộ cổng nào ra thế giới bên ngoài. Do đó, bạn có thể chọn lọc chỉ mở các cổng bạn cần để hiển thị các dịch vụ giao diện người dùng của mình.

Tiếp theo, xác định dịch vụ `webserver`:
```nginx
[label ~/node_project/docker-compose.yml]
...
 webserver:
    image: nginx:mainline-alpine
    container_name: webserver
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - web-root:/var/www/html
      - ./nginx-conf:/etc/nginx/conf.d
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
    depends_on:
      - nodejs
    networks:
      - app-network
```

Một số cài đặt được xác định ở đây cho dịch vụ `nodejs` vẫn giữ nguyên, nhưng một số thay đổi sau đã được thực hiện:

* `image`: Lệnh này yêu cầu Compose lấy hình ảnh Nginx dựa trên Alpine mới nhất từ Docker Hub. Để biết thêm thông tin về hình ảnh `alpine`, vui lòng đọc Bước 3 của Cách xây dựng ứng dụng Node.js bằng Docker.
* `ports`: Điều này hiển thị cổng `80` để kích hoạt các tùy chọn cấu hình mà bạn đã xác định trong cấu hình Nginx của mình.

Các volumes và bind mounts được đặt tên sau đây cũng được chỉ định:

* `web-root:/var/www/html`: Điều này sẽ thêm nội dung tĩnh của trang web của bạn, được sao chép vào một ổ đĩa có tên là `web-root`, vào thư mục `/var/www/html` trên vùng chứa.
* `./nginx-conf:/etc/nginx/conf.d`: Điều này sẽ liên kết gắn thư mục cấu hình Nginx trên máy chủ với thư mục có liên quan trên vùng chứa, đảm bảo rằng mọi thay đổi bạn thực hiện đối với các tệp trên máy chủ sẽ được phản ánh trong vùng chứa.
* `certbot-etc:/etc/letsencrypt`: Điều này sẽ gắn các chứng chỉ và khóa Let's Encrypt có liên quan cho miền của bạn vào thư mục thích hợp trên vùng chứa.
* `certbot-var:/var/lib/letsencrypt`: Điều này gắn kết thư mục làm việc mặc định của Let's Encrypt vào thư mục thích hợp trên vùng chứa.

Tiếp theo, thêm các tùy chọn cấu hình cho vùng chứa `certbot`. Đảm bảo thay thế thông tin tên miền và email bằng tên miền và email liên hệ của riêng bạn:
```nginx
[label ~/node_project/docker-compose.yml]
...
  certbot:
    image: certbot/certbot
    container_name: certbot
    volumes:
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
      - web-root:/var/www/html
    depends_on:
      - webserver
    command: certonly --webroot --webroot-path=/var/www/html --email <^>sammy@your_domain<^> --agree-tos --no-eff-email --staging -d <^>your_domain<^>  -d www.<^>your_domain<^>
```

Định nghĩa này yêu cầu Compose lấy hình ảnh certbot/certbot từ Docker Hub. Nó cũng sử dụng các volumes được đặt tên để chia sẻ tài nguyên với bộ chứa Nginx, bao gồm chứng chỉ miền và khóa trong `certbot-etc`, thư mục làm việc Let's Encrypt trong `certbot-var` và mã ứng dụng trong `web-root`.

Một lần nữa, bạn đã sử dụng `depends_on` để chỉ định rằng vùng chứa `certbot` sẽ được khởi động sau khi dịch vụ `webserver` đang chạy.

Tùy chọn `command` chỉ định lệnh sẽ chạy khi vùng chứa được khởi động. Nó bao gồm lệnh con `certonly` với các tùy chọn sau:

* `--webroot`: Điều này yêu cầu Certbot sử dụng plugin webroot để đặt các tệp vào thư mục webroot để xác thực.
* `--webroot-path`: Điều này chỉ định đường dẫn của thư mục webroot.
* `--email`: Email ưa thích của bạn để đăng ký và khôi phục.
* `--agree-tos`: Điều này xác định rằng bạn đồng ý với Thỏa thuận thuê bao của ACME.
* `--no-eff-email`: Điều này cho Certbot biết rằng bạn không muốn chia sẻ email của mình với Electronic Frontier Foundation (EFF). Hãy bỏ qua điều này nếu bạn muốn.
* `--staging`: Điều này cho Certbot biết rằng bạn muốn sử dụng môi trường chạy thử của Let's Encrypt để lấy chứng chỉ kiểm tra. Việc sử dụng tùy chọn này cho phép bạn kiểm tra các tùy chọn cấu hình của mình và tránh các giới hạn yêu cầu tên miền có thể có. Để biết thêm thông tin về các giới hạn này, vui lòng đọc tài liệu về giới hạn tốc độ của Let's Encrypt.
* `-d`: Điều này cho phép bạn chỉ định tên miền bạn muốn áp dụng cho yêu cầu của mình. Trong trường hợp này, bạn đã bao gồm `<^>your_domain<^>` và `www.<^>your_domain<^>`. Hãy chắc chắn thay thế chúng bằng tên miền của riêng bạn.

Bước cuối cùng, thêm định nghĩa volume và network. Hãy nhớ thay thế tên người dùng ở đây bằng non-**root** user của riêng bạn:
```nginx
[label ~/node_project/docker-compose.yml]
...
volumes:
  certbot-etc:
  certbot-var:
  web-root:
    driver: local
    driver_opts:
      type: none
      device: /home/<^>sammy<^>/<^>node_project<^>/views/
      o: bind

networks:
  app-network:
    driver: bridge
```

Volumes được đặt tên của bạn bao gồm chứng chỉ Certbot và volumes thư mục làm việc của bạn và volumes cho tài nguyên tĩnh trên trang web của bạn, `web-root`. Trong hầu hết các trường hợp, trình điều khiển mặc định cho tập Docker là trình điều khiển `local`, trên Linux chấp nhận các tùy chọn tương tự như lệnh `mount`. Nhờ điều này, bạn có thể chỉ định danh sách các tùy chọn trình điều khiển với `driver_opts`, gắn thư mục `views` trên máy chủ, trong đó chứa các tài nguyên tĩnh ứng dụng của bạn, vào volume khi chạy. Nội dung thư mục sau đó có thể được chia sẻ giữa các vùng chứa. Để biết thêm thông tin về nội dung của thư mục `views`, vui lòng đọc **Bước 2** về cách xây dựng ứng dụng Node.js với Docker.

Sau đây là tệp `docker-compose.yml` hoàn chỉnh:
```nginx
[label ~/node_project/docker-compose.yml]
version: '3'

services:
  nodejs:
    build:
      context: .
      dockerfile: Dockerfile
    image: nodejs
    container_name: nodejs
    restart: unless-stopped
    networks:
      - app-network

  webserver:
    image: nginx:mainline-alpine
    container_name: webserver
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - web-root:/var/www/html
      - ./nginx-conf:/etc/nginx/conf.d
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
    depends_on:
      - nodejs
    networks:
      - app-network

  certbot:
    image: certbot/certbot
    container_name: certbot
    volumes:
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
      - web-root:/var/www/html
    depends_on:
      - webserver
    command: certonly --webroot --webroot-path=/var/www/html --email <^>sammy@your_domain<^> --agree-tos --no-eff-email --staging -d <^>your_domain<^>  -d www.<^>your_domain<^> 

volumes:
  certbot-etc:
  certbot-var:
  web-root:
    driver: local
    driver_opts:
      type: none
      device: /home/<^>sammy<^>/<^>node_project<^>/views/
      o: bind

networks:
  app-network:
    driver: bridge
```

Với các định nghĩa dịch vụ được áp dụng bạn đã sẵn sàng khởi động vùng chứa và kiểm tra các yêu cầu chứng chỉ của mình.

## Bước 4 - Lấy chứng chỉ và thông tin xác thực SSL

Bạn có thể khởi động các vùng chứa bằng `docker-compose up`. Điều này sẽ tạo và chạy các vùng chứa và dịch vụ của bạn theo thứ tự bạn đã chỉ định. Sau khi yêu cầu miền của bạn thành công, chứng chỉ của bạn sẽ được gắn vào thư mục `/etc/letsencrypt/live` trên vùng chứa `webserver`.

Tạo các dịch vụ với `docker-compose up` bằng cờ `-d`, cờ này sẽ chạy các vùng chứa `nodejs` và `webserver` trong nền:
```command
docker-compose up -d
```

Đầu ra của bạn sẽ xác nhận rằng dịch vụ của bạn đã được tạo:
```
[secondary_label Output]
Creating nodejs ... <^>done<^>
Creating webserver ... <^>done<^>
Creating certbot   ... <^>done<^>
```

Sử dụng `docker-compose ps` để kiểm tra trạng thái dịch vụ của bạn:
```command
docker-compose ps
```

Nếu mọi thứ đều thành công, các dịch vụ `nodejs` và `webserver` của bạn sẽ `Up` và vùng chứa `certbot` sẽ thoát với thông báo trạng thái `0`:
```
[secondary_label Output]
  Name                 Command               State          Ports
------------------------------------------------------------------------
certbot     certbot certonly --webroot ...   Exit 0
nodejs      node app.js                      Up       8080/tcp
webserver   nginx -g daemon off;             Up       0.0.0.0:80->80/tcp
```

Nếu bạn nhận thấy bất cứ điều gì khác ngoài `Up` trong cột `State` đối với các dịch vụ `nodejs` và `webserver` hoặc trạng thái thoát khác `0` đối với vùng chứa `certbot`, hãy nhớ kiểm tra nhật ký dịch vụ bằng lệnh `docker-compose logs`. Ví dụ: nếu bạn muốn kiểm tra nhật ký Certbot, bạn sẽ chạy:
```command
docker-compose logs certbot
```

Bây giờ bạn có thể kiểm tra xem thông tin đăng nhập của bạn đã được gắn vào vùng chứa `webserver` hay chưa bằng `docker-compose exec`:
```command
docker-compose exec webserver ls -la /etc/letsencrypt/live
```

Khi yêu cầu của bạn thành công, đầu ra của bạn sẽ hiển thị như sau:
```
[secondary_label Output]
total 16
drwx------ 3 root root 4096 Dec 23 16:48 .
drwxr-xr-x 9 root root 4096 Dec 23 16:48 ..
-rw-r--r-- 1 root root  740 Dec 23 16:48 README
drwxr-xr-x 2 root root 4096 Dec 23 16:48 <^>your_domain<^>
```

Bây giờ bạn biết yêu cầu của mình sẽ thành công, bạn có thể chỉnh sửa định nghĩa dịch vụ `certbot` để xóa cờ `--staging`.

Mở tệp `docker-compose.yml`:
```command
nano docker-compose.yml
```

Tìm phần của tệp có định nghĩa dịch vụ `certbot` và thay thế cờ `--staging` trong tùy chọn `command` bằng cờ `--force-renewal`. Điều này sẽ cho Certbot biết rằng bạn muốn yêu cầu chứng chỉ mới có cùng tên miền với chứng chỉ hiện có. Định nghĩa dịch vụ `certbot` phải có các định nghĩa sau:
```nginx
[label ~/node_project/docker-compose.yml]
...
  certbot:
    image: certbot/certbot
    container_name: certbot
    volumes:
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
      - web-root:/var/www/html
    depends_on:
      - webserver
    command: certonly --webroot --webroot-path=/var/www/html --email <^>sammy@your_domain<^> --agree-tos --no-eff-email <^>--force-renewal<^> -d <^>your_domain<^> -d www.<^>your_domain<^>
...
```

Khi bạn chỉnh sửa xong, hãy lưu và thoát tệp. Bây giờ bạn có thể chạy `docker-compose up` để tạo lại vùng chứa `certbot` và các volumes liên quan của nó. Bằng cách bao gồm tùy chọn `--no-deps`, bạn đang thông báo cho Compose rằng Compose có thể bỏ qua việc khởi động dịch vụ `webserver` vì nó đã chạy:
```command
docker-compose up --force-recreate --no-deps certbot
```

Kết quả đầu ra sau đây cho biết yêu cầu chứng chỉ của bạn đã thành công:
```
[secondary_label Output]
Recreating certbot ... done
Attaching to certbot
certbot      | Account registered.
certbot      | Renewing an existing certificate for <^>your_domain<^> and www.<^>your_domain<^>
certbot      |
certbot      | Successfully received certificate.
certbot      | Certificate is saved at: /etc/letsencrypt/live/<^>your_domain<^>/fullchain.pem
certbot      | Key is saved at:         /etc/letsencrypt/live/<^>your_domain<^>                               phd.com/privkey.pem
certbot      | This certificate expires on 2022-11-03.
certbot      | These files will be updated when the certificate renews.
certbot      | NEXT STEPS:
certbot      | - The certificate will need to be renewed before it expires. Cert                               bot can automatically renew the certificate in the background, but you may need                                to take steps to enable that functionality. See https://certbot.org/renewal-setu                               p for instructions.
certbot      | Saving debug log to /var/log/letsencrypt/letsencrypt.log
certbot      |
certbot      | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -                                - - - - - - -
certbot      | If you like Certbot, please consider supporting our work by:
certbot      |  * Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/do                               nate
certbot      |  * Donating to EFF:                    https://eff.org/donate-le
certbot      | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -                                - - - - - - -
certbot exited with code 0
```

Với chứng chỉ đã có, bạn có thể chuyển sang sửa đổi cấu hình Nginx của mình để bao gồm SSL.

## Bước 5 - Sửa đổi cấu hình máy chủ web và định nghĩa dịch vụ

Việc bật SSL trong cấu hình Nginx của bạn sẽ liên quan đến việc thêm chuyển hướng HTTP vào HTTPS và chỉ định chứng chỉ SSL cũng như các vị trí chính của bạn. Nó cũng sẽ liên quan đến việc chỉ định nhóm Diffie-Hellman mà bạn sẽ sử dụng cho Perfect Forward Secrecy.

Vì bạn sắp tạo lại dịch vụ `webserver` để bao gồm những phần bổ sung này nên bạn có thể dừng dịch vụ đó ngay bây giờ:
```command
docker-compose stop webserver
```

Tiếp theo, tạo một thư mục trong thư mục dự án hiện tại của bạn cho khóa Diffie-Hellman:
```command
mkdir dhparam
```

Tạo khóa của bạn bằng lệnh `openssl`:
```command
sudo openssl dhparam -out /home/<^>sammy<^>/<^>node_project<^>/dhparam/dhparam-2048.pem 2048
```

Sẽ mất một chút thời gian để tạo khóa.

Để thêm thông tin Diffie-Hellman và SSL có liên quan vào cấu hình Nginx của bạn, trước tiên hãy xóa tệp cấu hình Nginx bạn đã tạo trước đó:
```command
rm nginx-conf/nginx.conf
```

Mở phiên bản khác của tệp:
```command
nano nginx-conf/nginx.conf
```

Thêm mã sau vào tệp để chuyển hướng HTTP sang HTTPS và thêm thông tin xác thực, giao thức và tiêu đề bảo mật SSL. Hãy nhớ thay thế `<^>your_domain<^>` bằng tên miền của riêng bạn:
```nginx
[label ~/node_project/nginx-conf/nginx.conf]
server {
        listen 80;
        listen [::]:80;
        server_name <^>your_domain<^> www.<^>your_domain<^>;

        location ~ /.well-known/acme-challenge {
          allow all;
          root /var/www/html;
        }

        location / {
                rewrite ^ https://$host$request_uri? permanent;
        }
}

server {
        listen 443 ssl http2;
        listen [::]:443 ssl http2;
        server_name <^>your_domain<^> www.<^>your_domain<^>;

        server_tokens off;

        ssl_certificate /etc/letsencrypt/live/<^>your_domain<^>/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/<^>your_domain<^>/privkey.pem;

        ssl_buffer_size 8k;

        ssl_dhparam /etc/ssl/certs/dhparam-2048.pem;

        ssl_protocols TLSv1.2;
        ssl_prefer_server_ciphers on;

        ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;

        ssl_ecdh_curve secp384r1;
        ssl_session_tickets off;

        ssl_stapling on;
        ssl_stapling_verify on;
        resolver 8.8.8.8;

        location / {
                try_files $uri @nodejs;
        }

        location @nodejs {
                proxy_pass http://nodejs:8080;
                add_header X-Frame-Options "SAMEORIGIN" always;
                add_header X-XSS-Protection "1; mode=block" always;
                add_header X-Content-Type-Options "nosniff" always;
                add_header Referrer-Policy "no-referrer-when-downgrade" always;
                add_header Content-Security-Policy "default-src * data: 'unsafe-eval' 'unsafe-inline'" always;
                #add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
                # enable strict transport security only if you understand the implications
        }

        root /var/www/html;
        index index.html index.htm index.nginx-debian.html;
}
```

Khối máy chủ HTTP chỉ định webroot cho các yêu cầu gia hạn Certbot tới thư mục `.well-known/acme-challenge`. Nó cũng bao gồm một lệnh viết lại hướng các yêu cầu HTTP đến thư mục gốc tới HTTPS.

Khối máy chủ HTTPS cho phép `ssl` và `http2`. Để đọc thêm về cách HTTP/2 lặp lại trên các giao thức HTTP và những lợi ích mà nó có thể mang lại cho hiệu suất trang web, vui lòng đọc phần giới thiệu về Cách thiết lập Nginx với hỗ trợ HTTP/2 trên Ubuntu 18.04. Khối này cũng bao gồm một loạt tùy chọn để đảm bảo rằng bạn đang sử dụng các giao thức và mật mã SSL cập nhật nhất cũng như tính năng ghim OSCP được bật. Việc ghim OSCP cho phép bạn đưa ra phản hồi theo thời gian từ cơ quan cấp chứng chỉ của bạn trong quá trình TLS handshake ban đầu, điều này có thể tăng tốc quá trình xác thực.

Khối này cũng chỉ định thông tin xác thực SSL và Diffie-Hellman cũng như các vị trí chính của bạn.

Cuối cùng, bạn đã di chuyển thông tin chuyển proxy sang khối này, bao gồm khối vị trí có lệnh `try_files`, trỏ yêu cầu tới vùng chứa ứng dụng Node.js bí danh của bạn và khối vị trí cho bí danh đó, bao gồm các tiêu đề bảo mật sẽ cho phép bạn để nhận được xếp hạng **A** về những thứ như trang web thử nghiệm máy chủ SSL Labs và Security Headers. Các tiêu đề này bao gồm `X-Frame-Options`, `X-Content-Type-Options`, `Referrer Policy`, `Content-Security-Policy`, và `X-XSS-Protection`. Tiêu đề HTTP `Strict Transport Security` (HSTS) đã được nhận xét - chỉ bật tiêu đề này nếu bạn hiểu ý nghĩa và đã đánh giá chức năng "preload" của nó.

Sau khi chỉnh sửa xong, hãy lưu và đóng tệp.

Trước khi tạo lại dịch vụ `webserver`, bạn cần thêm một số nội dung vào định nghĩa dịch vụ trong tệp `docker-compose.yml`, bao gồm thông tin cổng có liên quan cho HTTPS và định nghĩa volume Diffie-Hellman.

Mở tập tin:
```command
nano docker-compose.yml
```

Trong định nghĩa dịch vụ `webserver`, hãy thêm ánh xạ cổng dưới đây và volume có tên `dhparam`:
```nginx
[label ~/node_project/docker-compose.yml]
...
 webserver:
    image: nginx:latest
    container_name: webserver
    restart: unless-stopped
    ports:
      - "80:80"
      - <^>"443:443"<^>
    volumes:
      - web-root:/var/www/html
      - ./nginx-conf:/etc/nginx/conf.d
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
      - <^>dhparam:/etc/ssl/certs<^>
    depends_on:
      - nodejs
    networks:
      - app-network
```

Tiếp theo, thêm volume `dhparam` vào định nghĩa `volumes` của bạn. Hãy nhớ thay thế các thư mục `sammy` và `node_project` để khớp với thư mục của bạn:
```nginx
[label ~/node_project/docker-compose.yml]
...
volumes:
  ...
  webroot:
  ...
  <^>dhparam:<^>
    <^>driver: local<^>
    <^>driver_opts:<^>
      <^>type: none<^>
      <^>device: /home/sammy/node_project/dhparam/<^>
      <^>o: bind<^>
```

Tương tự như volume `web-root`, volume `dhparam` sẽ gắn khóa Diffie-Hellman được lưu trữ trên máy chủ vào vùng chứa `webserver`.

Lưu và đóng tệp khi bạn chỉnh sửa xong.

Tạo lại dịch vụ `webserver`:
```command
docker-compose up -d --force-recreate --no-deps webserver
```

Kiểm tra dịch vụ của bạn với `docker-compose ps`:
```command
docker-compose ps
```

Đầu ra sau cho biết rằng các dịch vụ `nodejs` và `webserver` của bạn đang chạy:
```
[secondary_label Output]
  Name                 Command               State                     Ports
----------------------------------------------------------------------------------------------
certbot     certbot certonly --webroot ...   Exit 0
nodejs      node app.js                      Up       8080/tcp
webserver   nginx -g daemon off;             Up       0.0.0.0:443->443/tcp, 0.0.0.0:80->80/tcp
```

Cuối cùng, bạn có thể truy cập miền của mình để đảm bảo rằng mọi thứ đều hoạt động như mong đợi. Điều hướng trình duyệt của bạn tới `https://<^>your_domain<^>`, đảm bảo thay thế `<^>your_domain<^>` bằng tên miền của riêng bạn:

![description image](https://media.cloudfly.vn/posts/landing_page_1.png)

Biểu tượng lock sẽ xuất hiện trong chỉ báo bảo mật của trình duyệt của bạn. Nếu muốn, bạn có thể điều hướng đến trang đích SSL Labs Server Test hoặc trang đích Security Headers server test. Các tùy chọn cấu hình được bao gồm sẽ giúp trang web của bạn được xếp hạng **A** trong SSL Labs Server Test. Để nhận được xếp hạng **A** trong Security Headers server test, bạn sẽ phải bỏ ghi chú tiêu đề **Strict Transport Security** (HSTS) trong tệp `nginx-conf/nginx.conf` của mình:
```nginx
[label ~/node_project/nginx-conf/nginx.conf]
…
location @nodejs {
                proxy_pass http://nodejs:8080;
                add_header X-Frame-Options "SAMEORIGIN" always;
                add_header X-XSS-Protection "1; mode=block" always;
                add_header X-Content-Type-Options "nosniff" always;
                add_header Referrer-Policy "no-referrer-when-downgrade" always;
                add_header Content-Security-Policy "default-src * data: 'unsafe-eval' 'unsafe-inline'" always;
                <^>add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;<^>
                # enable strict transport security only if you understand the implications
        }
…
```

Một lần nữa, chỉ bật tùy chọn này nếu bạn hiểu ý nghĩa và đã đánh giá chức năng “preload” của nó.

## Bước 6 - Gia hạn chứng chỉ

Chứng chỉ Let’s Encrypt có hiệu lực trong 90 ngày. Bạn có thể thiết lập quy trình gia hạn tự động để đảm bảo rằng chúng không bị mất hiệu lực. Một cách để làm điều này là tạo một công việc bằng tiện ích lập lịch trình `cron`. Bạn có thể lên lịch công việc `cron` bằng cách sử dụng tập lệnh sẽ gia hạn chứng chỉ và tải lại cấu hình Nginx của bạn.

Mở tập lệnh có tên `ssl_renew.sh` trong thư mục dự án của bạn:

```command
nano ssl_renew.sh
```

Thêm mã sau vào tập lệnh để gia hạn chứng chỉ và tải lại cấu hình máy chủ web của bạn:
```nginx
[label ~/node_project/ssl_renew.sh
]
#!/bin/bash

COMPOSE="/usr/local/bin/docker-compose --ansi never"
DOCKER="/usr/bin/docker"

cd /home/<^>sammy<^>/<^>node_project<^>/
$COMPOSE run certbot renew --dry-run && $COMPOSE kill -s SIGHUP webserver
$DOCKER system prune -af
```

Tập lệnh này trước tiên gán tệp nhị phân `docker-compose` cho một biến có tên `COMPOSE` và chỉ định tùy chọn `--no-ansi`, tùy chọn này sẽ chạy các lệnh `docker-compose` mà không có ký tự điều khiển ANSI. Sau đó nó thực hiện tương tự với `docker` nhị phân. Cuối cùng, nó thay đổi thư mục `~/node_project` và chạy các lệnh `docker-compose` sau:

* `docker-compose run`: Điều này sẽ khởi động vùng chứa `certbot` và ghi đè `command` được cung cấp trong định nghĩa dịch vụ `certbot`. Thay vì sử dụng lệnh con `certonly`, hãy sử dụng lệnh con `renew`, lệnh này sẽ gia hạn các chứng chỉ sắp hết hạn. Cũng bao gồm tùy chọn `--dry-run` để kiểm tra tập lệnh.
* `docker-compose kill`: Điều này sẽ gửi tín hiệu `SIGHUP` đến vùng chứa `webserver` để tải lại cấu hình Nginx.

Sau đó, nó chạy `docker system prune` để loại bỏ tất cả các vùng chứa và hình ảnh không sử dụng.

Đóng tệp khi bạn chỉnh sửa xong, sau đó làm cho nó có thể thực thi được:
```command
chmod +x ssl_renew.sh
```

Tiếp theo, mở tệp `crontab` **root** của bạn để chạy tập lệnh gia hạn theo một khoảng thời gian nhất định:

```custom_prefix()
sudo crontab -e 
```

Nếu đây là lần đầu tiên bạn chỉnh sửa tệp này, bạn sẽ được yêu cầu chọn một trình chỉnh sửa:
```nginx
[label crontab]
no crontab for root - using an empty one
Select an editor.  To change later, run 'select-editor'.
  1. /bin/ed
  2. /bin/nano        <---- easiest
  3. /usr/bin/vim.basic
  4. /usr/bin/vim.tiny
Choose 1-4 [2]: 
...
```

Ở cuối tập tin, thêm dòng sau:
```nginx
[label crontab]
...
*/5 * * * * /home/<^>sammy<^>/<^>node_project<^>/ssl_renew.sh >> /var/log/cron.log 2>&1
```

Thao tác này sẽ đặt khoảng thời gian thực hiện công việc là 5 phút một lần, do đó bạn có thể kiểm tra xem yêu cầu gia hạn của mình có hoạt động như dự kiến hay không. Bạn cũng đã tạo một tệp nhật ký, `cron.log`, để ghi lại kết quả đầu ra liên quan từ công việc.

Sau năm phút, hãy kiểm tra `cron.log` để xác nhận xem yêu cầu gia hạn có thành công hay không:
```command
tail -f /var/log/cron.log
```

Sau một lúc, đầu ra sau đây báo hiệu việc gia hạn thành công:
```
[secondary_label Output]
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates below have not been saved.)

Congratulations, all renewals succeeded. The following certs have been renewed:
  /etc/letsencrypt/live/<^>your_domain<^>/fullchain.pem (success)
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates above have not been saved.)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Killing webserver ... <^>done<^>
```

```
[secondary_label Output]
…
Congratulations, all simulated renewals succeeded: 
  /etc/letsencrypt/live/<^>your_domain<^>/fullchain.pem (success)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Killing webserver ... 
Killing webserver ... done
Deleted Containers:
00cad94050985261e5b377de43e314b30ad0a6a724189753a9a23ec76488fd78

Total reclaimed space: 824.5kB
```

Thoát ra bằng cách nhập `CTRL + C` trong thiết bị đầu cuối của bạn.

Bây giờ bạn có thể sửa đổi tệp `crontab` để đặt khoảng thời gian hàng ngày. Ví dụ: để chạy tập lệnh hàng ngày vào buổi trưa, bạn có thể sửa đổi dòng cuối cùng của tệp như sau:
```nginx
[label crontab]
...
0 12 * * * /home/<^>sammy<^>/<^>node_project<^>/ssl_renew.sh >> /var/log/cron.log 2>&1
```

Bạn cũng có thể xóa tùy chọn `--dry-run` khỏi tập lệnh `ssl_renew.sh` của mình:
```nginx
[label ~/node_project/ssl_renew.sh]
#!/bin/bash

COMPOSE="/usr/local/bin/docker-compose --no-ansi"
DOCKER="/usr/bin/docker"

cd /home/<^>sammy<^>/<^>node_project<^>/
$COMPOSE run certbot renew && $COMPOSE kill -s SIGHUP webserver
$DOCKER system prune -af
```

Công việc `cron` của bạn sẽ đảm bảo rằng chứng chỉ Let's Encrypt của bạn không bị mất hiệu lực khi gia hạn nếu chúng đủ điều kiện. Bạn cũng có thể thiết lập xoay vòng nhật ký bằng tiện ích Logrotate để xoay và nén các tệp nhật ký của mình.

## Kết luận

Bạn đã sử dụng vùng chứa để thiết lập và chạy ứng dụng Node với proxy ngược Nginx. Bạn cũng đã bảo mật chứng chỉ SSL cho miền ứng dụng của mình và thiết lập công việc `cron` để gia hạn các chứng chỉ này khi cần thiết.

Nếu bạn muốn tìm hiểu thêm về plugin Let's Encrypt, vui lòng xem lại bài viết của chúng tôi về cách sử dụng plugin Nginx hoặc plugin độc lập.

Bạn cũng có thể tìm hiểu thêm về Docker Compose bằng các tài nguyên sau:

* Cách cài đặt Docker Compose trên Ubuntu 18.04.
* Cách định cấu hình môi trường thử nghiệm tích hợp liên tục với Docker và Docker Compose trên Ubuntu 16.04.
* Cách thiết lập Laravel, Nginx và MySQL với Docker Compose.
