Tác giả: Thức Nguyễn

Thông tin liên hệ:


Mình xin tự giới thiệu mình là Thức Nguyễn. Hiện đang là 1 dev FE và đang học thêm để trở thành 1 DevOps Engineer trong tương lai. Đây là bài viết đầu tiên cùa mình trên devopsedu.vn. Bài viết hướng dẫn về cách dockerize 1 ứng dụng react app với non root user. Trong quá trình mình viết cũng sẽ không tránh được những thiếu sót hy vọng sẽ nhận được các góp ý của mọi người. Mọi thắc mắc về bài viết mọi người có thể gửi về cho mình qua mail vanthuc1994@gmail.com. Xin cám ơn mọi người. Giờ chúng ta sẽ đi vào bài viết thôi nào.

Chuẩn bị

  1. Cài đặt 1 server (Các bạn có thể tham khảo cách cài đặt server ở local trong khóa devop for fresher).
    • Chú ý server mình dùng ở đây là ubuntu 20.04
  2. Cài đặt Nodejs version 18 (tham khảo cài đặt tại link này)
    • Có thể tham khảo các câu lệnh bên dưới nếu muốn thực hành nhanh

apt-get update

curl -s https://deb.nodesource.com/setup_18.x | sudo bash

apt install nodejs -y

  • Chú ý nên chuyển qua user root trước khi chạy các câu lệnh này.
  • Khi cài đặt thành công chạy 2 lệnh bên dưới để xem version của node và của npm
    • node -v
    • npm -v
  • Kết quả hiển thi như bên dưới là đã cài đặt node thành công rồi

3. Cài đặt docker (tham khảo cài đặt tại link này)

    • Có thể tham khảo nhanh các câu lệnh bên dưới
      • apt update
      • apt install -y apt-transport-https ca-certificates curl software-properties-common
      • curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
      • echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
      • apt update
      • apt install docker-ce -y
    • Kiểm tra cài docker đã thành công hay chưa bằng câu lệnh systemctl status docker

Hướng dẫn Dockerize reactpp

Chú ý chúng ta sẽ tuân theo nguyên tắc dưới đây

  1. Mỗi dự án sẽ được chạy ở 1 thư mục riêng
  2. Mỗi dự án cũng sẽ được chạy bởi 1 user riêng (chứ không dùng user root để tránh việc hacker nếu có hack được dự án của chúng ta thì cũng sẽ không ảnh hưởng đến các dự án khác đang chạy)

Tạo 1 user mới để chạy dự án

Đầu tiên chúng ta cần tạo mới 1 user có tên là reactapp bằng câu lệnh bên dưới (chú ý chạy bằng user root nhé)

adduser reactapp

Sau đó chúng ta chuyển qua user reactapp và tạo 1 thư mục mới có tên là projects trong /home/reactapp

Chú ý thư mục projects sẽ là nơi chứa các dự án mà chúng ta sẽ chạy khi sử dụng user reactapp

Tạo mới dự án react js

Trước hết chúng ta vào trang chủ của create react app và chạy các câu lệnh sau

npx create-react-app react-app (Câu lệnh này để tạo 1 project react js và project này chúng ta sẽ dùng dockerize react app)

chmod -R 750 react-app (Lệnh này để giới hạn quyền đối với thư mục react-app)

    • Đối với user react-app thì sẽ có full quyền (read-write-execute).
    • Đối với group react-app thì sẽ chỉ có quyền read và execute.
    • Đối với các user khác thì không có quyền gì cả đối với project của chúng ta.

cd react-app (Di chuyển vào thư mục làm việc của chúng ta)

     

npm run build (Chạy lệnh này để build thử dự án của chúng ta xem có thành công hay không. Nếu chạy ra kết quả như bên dưới tức là chúng ta đã build thành công)

Dockerize react app

Tạo file .dockerignore trong thư mục react-app có nội dung như bên dưới

node_modules/
build/
.git
*.md
.dockerignore
Dockerfile

Chú ý: Việc tạo file .dockerignore này để loại bỏ các file hoặc thư mục chúng ta không muốn copy vào trong docker khi thực hiện docker build. Việc này sẽ giúp chúng ta build nhanh hơn và kích thước image cuối cùng chúng ta build ra cũng nhẹ hơn vì chỉ chứa các thành phần cần thiết cho ứng dụng

Tạo file nginx.conf

worker_processes 1;
error_log /var/log/nginx/error.log warn;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main ‘$remote_addr – $remote_user [$time_local] “$request” ‘
‘$status $body_bytes_sent “$http_referer” ‘
‘”$http_user_agent” “$http_x_forwarded_for”‘;
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name webclient;
location / {
root /app;
index index.html;
try_files $uri $uri/ /index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}

   }
}

     Hình bên dưới mình minh họa cấu hình cho các bạn hình dung rõ hơn nhé

Chú ý nếu muốn thay đổi đường của các file log hay thư mục build source code của dự án thì chúng ta sẽ thay đổi tương ứng giống như mô tả của hình bên trên nhé.

Tạo Dockerfile

FROM node:18-alpine@sha256:17514b20acef0e79691285e7a59f3ae561f7a1702a9adc72a515aef23f326729 AS base

FROM base AS build
WORKDIR /app
COPY package.json package-lock.json .
RUN npm install
COPY . .
RUN npm run build

FROM nginx:1.27-alpine-slim@sha256:2be9e698d136d4d9be33d1852b1259bc1b80e20aed0c964cbcd6086da7fad5c7
WORKDIR /app
COPY –from=build /app/build .
COPY –from=build /app/nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD [“nginx”, “-g”, “daemon off;”]

Build image với câu lệnh sau

docker build -t react-app-root-user .

Nếu chúng ta gặp lỗi bên dưới thì do react-app user chưa nằm trong group docker nên chạy build bị lỗi (Mặc định chỉ root user và các user trong group docker mới có thể chạy được docker daemon)

Cách giải quyết là chúng ta thêm group docker cho user react-app như bên dưới (chú ý su qua user root để thực hiện nhé)

Chạy ra kết quả như bên dưới là thành công nhé

Run image

chạy với câu lệnh sau

docker run -d -p 3003:80 react-app-root-user

Nếu chạy ra kết quả như bên dưới là thành công nhé

Đến đây chúng ta hoàn thành việc dockerize react app và chạy chúng trên nginx. Tuy nhiên nếu chúng ta để ý kỹ hơn thì các tiến trình trong docker đang chạy bằng user root. Chúng ta có thể xem ảnh minh họa bên dưới để hiểu rõ hơn

Khi sh vào container chúng ta gõ lệnh top để hiển thị được giao diện bên dưới nhé

Viêc chạy bằng user root có thể khiến ứng dụng của chúng kém bảo mật hơn vì vậy chúng ta sẽ chạy cấu hình để chạy container với quyền non root thông qua instruction USER trong dockerfile.

Dựa theo doc của nginx trên docker hub cấu hình chạy nginx với non root user chúng ta sẽ thay đổi file nginx.conf và Dockerfile như sau

Thay đổi file nginx.conf

worker_processes 1;
pid /tmp/nginx.pid;
error_log /tmp/log/nginx/error.log warn;
events {
worker_connections 1024;
}
http {
client_body_temp_path /tmp/client_temp;
proxy_temp_path /tmp/proxy_temp_path;
fastcgi_temp_path /tmp/fastcgi_temp;
uwsgi_temp_path /tmp/uwsgi_temp;
scgi_temp_path /tmp/scgi_temp;
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main ‘$remote_addr – $remote_user [$time_local] “$request” ‘
‘$status $body_bytes_sent “$http_referer” ‘
‘”$http_user_agent” “$http_x_forwarded_for”‘;
access_log /tmp/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name webclient;
location / {
root /app;
index index.html;
try_files $uri $uri/ /index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
}

Chú ý trong file nginx.conf chúng ta có 1 vài thay đổi

  1. Thay đổi đường dẫn lưu nginx.pid
    • pid /tmp/nginx.pid;
  2. Thay đổi đường dẫn lưu error_log và access_log
    • error_log /tmp/log/nginx/error.log warn;
    • access_log /tmp/log/nginx/access.log main;
  3. Thêm các config vào trong http context
    • client_body_temp_path /tmp/client_temp;
    • proxy_temp_path /tmp/proxy_temp_path;
    • fastcgi_temp_path /tmp/fastcgi_temp;
    • scgi_temp_path /tmp/scgi_temp;

Thay đổi Dockerfile

FROM node:18-alpine@sha256:17514b20acef0e79691285e7a59f3ae561f7a1702a9adc72a515aef23f326729 AS base

FROM base AS build
WORKDIR /app
COPY package.json package-lock.json .
RUN npm install
COPY . .
RUN npm run build

FROM nginx:1.27-alpine-slim@sha256:2be9e698d136d4d9be33d1852b1259bc1b80e20aed0c964cbcd6086da7fad5c7
WORKDIR /app
RUN addgroup -g 10001 reactapp &&
adduser -D -u 10001 -G reactapp reactapp &&
chown -R reactapp:reactapp /app &&
chmod -R 750 /app &&
chown -R reactapp:reactapp /var/cache/nginx &&
mkdir -p /tmp/log/nginx &&
touch /tmp/nginx.pid &&
chown -R reactapp:reactapp /tmp/nginx.pid &&
chown -R reactapp:reactapp /tmp/log/nginx
COPY –from=build –chown=reactapp:reactpp –chmod=750 /app/build .
COPY –from=build –chown=reactapp:reactapp /app/nginx.conf /etc/nginx/nginx.conf
USER reactapp
EXPOSE 80
CMD [“nginx”, “-g”, “daemon off;”]

Chú ý ở trong Dockerfile này chúng ta có 1 vài thay đổi sau

  1. Tạo group user có tên là reactapp với GID là 10001
  2. Tạo user reactapp có uid là 10001 và thuộc group reactapp
  3. Trong thư mục /app
    • gán lại user sở hữu và group sở hữu về user reactapp và group react app
    • Phân quyền 750 cho thư mục này.
  4. Gán lại quyền sở hữu và nhóm sở hữu thư mục /var/cache/nginx về user reactapp và group react app
  5. Tạo thư mục lưu trữ access_log và error_log tại thư mục /tmp/log/nginx
    • gán lại user sở hữu và group sở hữu về user reactapp và group react app.
  6. Tạo file /tmp/nginx.pid
    • gán lại user sở hữu và group sở hữu về user reactapp và group react app.
  7. Sao chép thư mục build source code từ /app/build trong stage build vào thư mục /app của stage hiện tại và đổi quyền sở hữu cùng phân quyền.
  8. Ghi đè file cấu hình nginx.conf
  9. Chỉ định user sẽ chạy trong container (ở đây là reactapp user)

Build lại dockerfile bằng câu lệnh bên dưới

docker build -t react-app-non-root-user .

Chạy lại container ở cổng 3004

docker run -d -p 3004:80 react-app-non-root-user

Kiểm tra lại xem container của chúng ta đã chạy với non-root user hay chưa

Như vậy ứng dụng của chúng ta đã chạy bằng non-root user đúng như yêu cầu rồi. Chúng ta sẽ xem kết quả chạy ứng dụng tại port 3004 như bên dưới nhé

Bài viết này chắc đã quá dài rồi nên mình xin kết thúc tại đây. Hy vọng với những chia sẽ trên của mình sẽ giúp ích được phần nào đó cho các bạn. Chúc các bạn 1 ngày cuối tuần vui vẻ bên gia đình và người thân. Bye bye