===
Tóm tắt quy trình cài:
# sudo apt update && sudo apt upgrade -y
# sudo apt install -y nginx git
# curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash –
# sudo apt install -y nodejs
# wget -qO – https://www.mongodb.org/static/pgp/server-3.4.asc | sudo apt-key add –
# echo “deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu xenial/mongodb-org/3.4 multiverse” | sudo tee /etc/apt/sources.list.d/mongodb-org-3.4.list
# sudo apt update
# sudo apt install -y mongodb-org
# sudo systemctl start mongod
# sudo systemctl enable mongod
# sudo mkdir -p /apps/my-node-app
Chép code vào trong thư mục my-node-app vừa tạo
# npm install
# sudo nano /etc/systemd/system/my-node-app.service
xem tiếp phía dưới nha ^^!
===
Giới Thiệu
NodeJS tuy phát triển mạnh mẽ những năm gần đây nhưng tại Việt Nam vẫn còn ít lựa chọn để triển khai (deploy) lên Internet cho công chúng. Các dịch vụ “app engine” cho NodeJS của nước ngoài như Heroku, Nodejitsu, now.sh… khó tiếp cận cho developer ở Việt Nam. Ngoài ra các dịch vụ trên còn nhiều hạn chế chẳng hạn: chưa có data center đặt tại Việt Nam, không kèm giải pháp database hoặc nếu có cũng là dạng add-on.
Để cài đặt một web app viết bằng NodeJS, cách đơn giản nhất và tiết kiệm chi phí nhất hiện nay chính là sử dụng Cloud VPS[^1]. Cloud VPS là một server toàn quyền được ảo hóa trên một server vật lý, có tài nguyên và băng thông được dành riêng. Ở Việt Nam, hiện đã có nhiều nhà cung cấp dịch vụ Cloud VPS với chi phí khá tiết kiệm (cấu hình thấp nhất có chi phí khoảng 150k/tháng). (Vì đây là bài hướng dẫn kỹ thuật nên tôi sẽ không giới thiệu cụ thể nhà cung cấp nào).
Trong bài viết này, tôi sẽ hướng dẫn bạn cài đặt một NodeJS app (cụ thể viết bằng KeystoneJS) lên một VPS Ubuntu 16.04 LTS[^2] với kết quả mong muốn như sau:
Website tải cực nhanh
Các tài nguyên tĩnh có cache header để tối ưu thời gian tải cho lần xem sau
Các tài nguyên tĩnh được gzip để tối ưu băng thông
Được mã hóa TLS với chữ ký số hợp lệ (miễn phí)
Có thể cài đặt và chạy thêm app trên cùng server
Cài Đặt Serverper
Sau khi bạn tạo một Cloud VPS mới, thường thì nó sẽ ở trạng thái mặc định, tức là chưa có thêm bất cứ ứng dụng nào khác được cài thêm. Một số nhà cung cấp VPS thì có thể “nhanh nhảu” cài sẵn Apache HTTP server, tuy nhiên chúng ta sẽ không dùng Apache trong hướng dẫn này, nên có thể bạn phải cần remove nó khỏi hệ thống.
Ngoài ra, tùy nhà cung cấp VPS mà bạn sẽ được cấp tài khoản root hoặc tài khoản user bình thường nhưng có quyền sudo. Trong trường hợp bạn có tài khoản root thì sẽ không cần chạy những dòng lệnh bắt đầu bằng sudo[^3].
Bạn sẽ truy cập vào server thông qua giao thức SSH và sẽ cấu hình server bằng dòng lệnh trong một cửa sổ terminal. Nếu bạn dùng Windows, tham khảo hướng dẫn kết nối SSH vào Linux server tại đây và đây
SSH terminal window
Giao diện dòng lệnh SSH terminal trên macOS
Trước khi tiến hành cài đặt các phần mềm cần thiết, bạn cần cập nhật server Ubuntu với những package mới nhất:
sudo apt update && sudo apt upgrade -y
Cài đặt NGINX và Git
Đầu tiên chúng ta sẽ cài 2 phần mềm có sẵn trong repository của Ubuntu: nginx & git
sudo apt install -y nginx git
NGINX sẽ đóng vai trò reversed proxy và static file server để tiếp nhận request thông qua port mặc định 80 (http) và 443 (https). Git dùng để lấy source code của app để tiến hành build và deploy.
Cài đặt NodeJS VM:
Tùy vào yêu cầu phiên bản Node của web app, bạn sẽ cài phiên bản Node engine tương ứng. Ở đây chúng ta sẽ dùng Node 8 LTS làm ví dụ.
Thêm repository source cho NodeJS 8 LTS (link tham khảo):
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash –
Sau đó cài NodeJS lên server:
sudo apt install -y nodejs
Hướng dẫn này chỉ sử dụng một phiên bản Node. Nếu có yêu cầu cài đặt nhiều app trên cùng một server và sử dụng nhiều phiên bản Node khác nhau, bạn cân nhắc cài đặt Node thông qua trình quản lý nhiều phiên bản Node như nvm hoặc n package
Cài đặt MongoDB:
wget -qO – https://www.mongodb.org/static/pgp/server-3.4.asc | sudo apt-key add –
Thêm repository source của MongoDB dành riêng cho Ubuntu 16.04:
echo “deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu xenial/mongodb-org/3.4 multiverse” | sudo tee /etc/apt/sources.list.d/mongodb-org-3.4.list
Tiến hành cài MongoDB:
sudo apt update && sudo apt install -y mongodb-org
Khởi động dịch vụ mongod lần đầu tiên và đăng ký để nó tự chạy lúc restart server:
# Khởi động mongod service
sudo systemctl start mongod
# Bật chức năng tự chạy khi restart Ubuntu
sudo systemctl enable mongod
Tất cả trong một
Trên đây là hướng dẫn từng bước để các bạn hiểu rõ mình đang làm những gì để khởi tạo server mới. Một khi đã hiểu rõ, bạn có thể viết tất cả những dòng lệnh đó vào một file bash script như ở Gist này và thực thi nó:
# Thêm quyền thực thi cho file bash script
chmod +x initialize.sh
# Chạy file bash script này
./intialize.sh
Tải mã nguồn của app từ Git host và build app
Trong hướng dẫn này, chúng ta sẽ lấy mã nguồn của app từ một trong 3 Git host phổ biến là Github, Bitbucket, và Gitlab và build ngay tại server.
Trước tiên, chúng ta sẽ tạo khóa SSH mặc định trên server bằng các bước sau (Enter mặc định với tất cả các câu hỏi):
ssh-keygen -t rsa -b 4096 -C “your_email@example.com”
Xuất nội dung của public key ra terminal để copy:
cat ~/.ssh/id_rsa.pub
Tại trang settings của project trên Git host, thêm deploy key và paste nội dung của file id_rsa.pub vừa mới copy ở trên.
Tại Bitbucket, vào Settings > Chọn tiếp Access Keys
Tại Github, vào Settings > Chọn Deploy Keys
Tại Gitlab, vào Settings > Repository > Deploy Keys
Add deploy key
Ảnh chụp popup thêm Access key của Bitbucket.
Quay trở lại terminal của server, tạo thư mục để chứa mã nguồn của app sẽ được clone vào:
sudo mkdir -p /apps/my-node-app
# Đổi owner của thư mục về user hiện tại để tiện chạy các lệnh sau đó
sudo chown -R $USER /apps/my-node-app/
Lệnh ở trên sẽ tự động tạo 2 cấp thư mục /apps/my-node-app tại root.
Theo quy ước, các app 3rd-party cài trên Linux thường được cài tại thư mục /opt. Tuy nhiên, đây là ứng dụng đặc biệt do chúng ta viết riêng nên chúng ta sẽ cài vào /apps để tách bạch.
Tiếp theo, clone Git repo của app vào thư mục vừa tạo (lưu ý sử dụng lệnh clone với giao thức SSH).
git clone git@bitbucket.org:
/ .git /apps/my-node-app/
Với bản Gitlab mới nhất, bạn còn một lựa chọn nữa để lấy source từ Gitlab đó là dùng Deploy Token. Khi đó, URL để clone source có dạng: https://
Build app
Với một Node.js app chuẩn, việc đầu tiên chúng ta cần làm là cd vào thư mục gốc của project, và chạy lệnh cài tự động các dependency package được liệt kê trong package.json:
npm install
Sau đó, tùy vào cài đặt của dự án, ta cần chạy các lệnh để build các thành phần cần biên dịch hoặc tối ưu hóa từ mã nguồn.
Theo thông lệ chung của các NodeJS app, việc build project sẽ thông qua một lệnh script được config sẵn trong package.json, VD:
{
“scripts”: {
“build”: “gulp build”
}
}
Do đó, việc tiếp theo sẽ là chạy script build này bằng npm:
npm run build
Tới đây, chúng ta đã có thể chạy thử app bằng lệnh node keystone.js (giả sử keystone.js là điểm start của app) và preview tại IP của server và port mặc định 3000 (VD: http://12.34.56.789:3000).
Với những bước cài đặt vừa rồi, bạn đã có thể chạy app cho môi trường TEST hoặc STAGING và có thể demo với khách hàng. Để giữ cho app demo tiếp tục chạy sau khi thoát SSH, bạn có thể dùng dòng lệnh screen (xem hướng dẫn).
//=========== Phần 2 ===========================
Hạn chế quyền truy cập của app
Đây là bước không bắt buộc, tuy nhiên tôi khuyến cáo không nên bỏ qua, nhất là bạn đang truy cập vào VPS bằng tài khoản root. Chúng ta sẽ tạo một Linux user mới và gán cho thư mục của app.
# Tạo một Linux user mới, nên dùng tên của app và không để khoảng trắng
sudo useradd my-node-app
# Chuyển quyền sở hữu (owner) thư mục app và các thư mục con cho user mới
sudo chown -R my-node-app /apps/my-node-app
Trong bước tiếp theo, khi chạy app như dịch vụ, app sẽ được chạy với quyền của user mới này để hạn chế quyền truy cập của app lên các thư mục hệ thống.
Chạy app như dịch vụ
Tiếp theo chúng ta sẽ cài đặt để chạy app như dịch vụ và tự động chạy lại khi server được restart. Có một số tutorial hướng dẫn chạy app với pm2, nhưng hôm nay tôi sẽ hướng dẫn dùng công cụ quản lý ứng dụng dịch vụ systemd trên Ubuntu 16 và các HĐH Linux mới.
Đầu tiên, chúng ta sẽ chạy một số lệnh để lấy thông tin cho config systemd:
# Lấy đường dẫn tuyệt đối đến node VM
which node
# -> /usr/bin/node# Lấy đường dẫn tuyệt đối đến thư mục của app
pwd
# -> /apps/my-node-app# Lấy thông tin user hiện tại (bỏ qua nếu chúng ta đã tạo user ở trên)
id
# -> uid=0(root) gid=0(root) groups=0(root)
Tiếp theo, tạo file service config cho systemd:
sudo nano /etc/systemd/system/my-node-app.service
nano là editor trên terminal dễ dùng nhất. Bạn có thể thay nano bằng vim nếu nó là editor quen thuộc với bạn hơn.
Tên file my-node-app.service là do bạn đặt nhưng cần giữ nguyên đuôi .service
Paste vào editor nội dung sau:
[Unit]
Description=Thay bằng nội dung mô tả app của bạn
Requires=mongod.service
After=mongod.service[Service]
ExecStart=/usr/bin/node /apps/my-node-app/keystone.js
WorkingDirectory=/apps/my-node-app
Restart=always
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=my-node-app
User=my-node-app
Group=my-node-app
Environment=PORT=3000
Environment=MONGO_URL=mongodb://localhost:27017/my-node-app[Install]
WantedBy=multi-user.target
Một vài điều lưu ý khi chỉnh sửa file .service trên:
Hai thuộc tính Requires và After để cho systemd biết chỉ kích hoạt dịch vụ này khi service chỉ định tại Requires và After đã được chạy (Trong trường hợp server phải restart và có rất nhiều service cần chạy lúc khởi động). Trong ví dụ này, mongod.service là tên service của MongoDB khi được cài như hướng dẫn trong phần một.
ExecStart là nơi thực thi lệnh cho dịch vụ, và yêu cầu đường dẫn phải tuyệt đối.
WorkingDirectory là thư mục ngữ cảnh khi lệnh được thực thi.
SyslogIdentifier là tên định danh của dịch vụ trong logger hệ thống, chúng ta sẽ dùng nó để lọc các output hoặc báo lỗi của app như trong phần tiếp theo.
User và Group là tên của Linux user mà chúng ta muốn app sử dụng khi chạy. Như đã nói ở trên, bạn có thể dùng user mới tạo là my-node-app hoặc tên của user trả về ở dòng lệnh id.
Environment là nơi để bạn gán các biến môi trường lúc thực thi app. Với Keystonejs thì có thể bạn không cần sử dụng thuộc tính này vì nó sử dụng .env (dotenv).
Những thuộc tính còn lại nên để giá trị như ví dụ.
Sau khi bạn đã tạo xong file .service, bạn có thể thử chạy app ngay thông qua lệnh sau:
# Bắt đầu chạy app như dịch vụ (không cần đường dẫn đến file .service)
sudo systemctl start my-node-app.service# Kiểm tra trạng thái app đang chạy hay không
sudo systemctl status my-node-app.service
# -> Nếu app chạy thành công, output sẽ có dòng: …Active: active (running)…
Cuối cùng, sau khi app đã được chạy thành công và bạn có thể vào qua port 3000, bật chức năng dịch vụ tự khởi động khi server được restart:
sudo systemctl enable my-node-app.service
# -> Created symlink from … to …
Proxy app ra cổng 80 bằng NGINX
Nginx sẽ đóng vai trò reversed proxy và static file server. Nó sẽ tiếp nhận request từ ngoài Internet thông qua port mặc định 80 (http) và 443 (https) và forward request qua port của app là 3000.
Đối với Ubuntu[^1], 2 file config chính của Nginx nằm ở: /etc/nginx/nginx.conf (config toàn server) và /etc/nginx/sites-available/default (config cho từng web host ảo[^2]).
Bước này yêu cầu bạn đã cấu hình DNS của domain trỏ đến địa chỉ IP tĩnh của VPS (VD: mynodeapp.com). Sau đó bạn vào chỉnh sửa file /etc/nginx/sites-available/default, thay toàn bộ nội dung mặc định bằng mẫu bên dưới.
server {
listen 80 default_server;
listen [::]:80 default_server;# File mặc định khi vào thư mục
index index.html index.htm;# Điều chỉnh kích thước gói upload tối đa
client_max_body_size 25M;# Domain name của web app, có thể một hoặc nhiều domain cùng trỏ đến
server_name mynodeapp.com www.mynodeapp.com;# Forward toàn bộ request sang web app
location / {
# Thay đổi port nếu node-web-app chạy trên port khác 3000
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection ‘upgrade’;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
Nếu muốn dùng cùng config cho nhiều web host ảo, bạn có thể chuyển một số directive ở trên như index, client_max_body_size vào trong config toàn server tại /etc/nginx/nginx.conf.
Sau khi chỉnh sửa xong Nginx config như trên. Bạn có thể thử kiểm tra config mới có hợp lệ hay không bằng lệnh:
sudo nginx -t
# -> nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# -> nginx: configuration file /etc/nginx/nginx.conf test is successful
Nếu config mới OK, bạn khởi động lại Nginx bằng lệnh sau:
sudo systemctl restart nginx
Sau khi Nginx được restart, bạn vào thử website tại domain đã cài đặt ở trên với URL không thêm port (http://my-node-app.com). Nếu website hiện ra thì bạn đã cài đặt thành công. Nếu bạn thấy lỗi “Bad Gateway”, tức là cấu hình Nginx vừa rồi chưa thành công và cần phải rà soát lại.
Serve file tĩnh bằng NGINX
Một trong những lý do tôi hướng dẫn bạn sử dụng Nginx, ngoài việc để bật https, còn là để serve file tĩnh (static file) hiệu quả hơn. Nếu như các file tĩnh của web app được thiết kế sử dụng CDN thì bạn có thể không cần bước này.
Nginx serve file tĩnh cực nhanh với lượng kết nối đồng thời cao (concurrency). Việc bật header cache-ontrol với Nginx rất dễ dàng sẽ giúp tăng hiệu quả tải trang. Ngoài ra bạn còn có thể bật gzip khi serve file tĩnh, là một yêu cầu không thể thiếu khi tối ưu hóa việc tải trang từ phía server.
Để serve file tĩnh, chúng ta sẽ thêm bộ lọc location vào block server của host config ở trên. Đối với KeystoneJS, chúng ta có một thư mục file tĩnh mặc định đó là public và với ví dụ từ đầu đến giờ, đường dẫn đến thư mục này sẽ là /apps/my-node-app/public. Ngoài ra, nếu bạn có một thư mục để upload riêng và nằm ngoài thư mục public này, thì bạn cũng cần ghi lại đường dẫn để config như tiếp theo sau đây:
# thêm directive location trong config /etc/nginx/sites-available/default
server {
# …Các config khác đã ẩn…# Hướng dẫn cho Nginx serve tĩnh các file và folder bên trong public,
# có tên bắt đầu bằng một trong những pattern như bên dưới
location ~ ^/(fonts/|images/|javascript/|js/|script/|css/|stylesheets/|flash/|media/|static/|upload/|robots.txt|humans.txt|favicon.ico) {
# đường dẫn tuyệt đối đến thư mục file tĩnh
root /apps/my-node-app/public;
access_log off;
# bật cache-control lên với thời gian expire tối đa
expires max;
}# Nếu bạn có thư mục upload bên ngoài, bạn cần thêm một directive `location` đến thư mục này
# Xem document của Nginx để biết thêm cách cấu hình location# Đặt directive location / (app reversed proxy) ở dưới cùng
location / {
# …
}
}
Một vài vấn đề có thể gặp với việc serve file tĩnh bằng Nginx:
Nếu bạn bị lỗi 403 Forbidden: khả năng là Nginx chưa có quyền truy cập thư mục của app. Bạn cần thêm quyền truy cập cho thư mục public bằng lệnh sudo chmod o+rx -R public.
Để bật gzip, bạn có thể vào file /etc/nginx/nginx.conf, và bỏ comment (dấu #) ở những dòng config gzip như bên dưới:
http {
# …Các config khác đã ẩn…##
# Gzip Settings
##gzip on;
gzip_disable “msie6”;gzip_vary on;
gzip_proxied any;
# gzip_comp_level 6;
# gzip_buffers 16 8k;
# gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
}
Trong danh sách các loại file tĩnh được nén với gzip ở trên (gzip_types), tôi đã thêm SVG. Bạn có thể thêm các loại file khác nếu danh sách chưa có, nhưng không nên thêm các file ảnh bởi chúng đã được nén bằng thuật toán riêng và sẽ không hiệu quả khi nén tiếp với gzip.
Lấy chữ ký số từ Let’s Encrypt và cài đặt cho NGINX
Để một trang web được mã hóa an toàn thông qua giao thức HTTPS, bạn cần một chứng thư số (certificate) TLS/SSL để chứng thực cho những domain mà bạn sử dụng. TLS/SSL certificate phải được ký bởi một CA (Certificate Authority) hợp lệ, và trước đây chúng ta phải trả một khoản phí để đăng ký với CA. Tuy nhiên, từ bây giờ chúng ta đã có thêm Let’s Encrypt là một CA cung cấp TLS/SSL certificate hoàn toàn miễn phí.
Có nhiều cách để cài đặt certificate Let’s Encrypt nhưng đơn giản nhất trên Linux kết hợp với Nginx là sử dụng công cụ dòng lệnh Certbot. Ở đây tôi sẽ tóm tắt các bước cần thực hiện:
# Chạy các lệnh sau để cài đặt lệnh certbot kèm plugin Nginx trên Ubuntu
sudo apt-get update
sudo apt-get install software-properties-common
sudo add-apt-repository ppa:certbot/certbot
sudo apt-get update
sudo apt-get install python-certbot-nginx
Sau khi certbot được cài, bạn có thể chạy lệnh sau để bắt đầu tiến trình cài đặt certificate cho website với Nginx plugin:
sudo certbot –nginx
Sau khi gọi lệnh trên, bạn sẽ nhập thông tin theo yêu cầu:
Đầu tiên, certbot sẽ đọc Nginx config và liệt kê các tên miền hiện có. Bạn sẽ nhập vào con số gắn với tên miền (1, 2…) hoặc Enter bỏ trống để chọn tất cả.
Certbot cũng sẽ hỏi bạn có muốn thêm Nginx config để tự động redirect từ HTTP sang HTTPS không[^3]. Tùy vào yêu cầu dự án, nhưng khả năng cao là bạn nên chọn “2: Redirect”.
Certbot cũng sẽ hỏi một số thông tin về email liên lạc và một số thỏa thuận khác.
Certbot sẽ tự động xác nhận quyền sở hữu domain thông qua Nginx server và domain của bạn đã được trỏ về IP của VPS trước đó. Certbot cũng sẽ tự động lưu certificate xuống VPS, cấu hình lại Nginx config và khởi động lại Nginx giùm bạn. Ngay sau khi kết thúc cài đặt với certbot, bạn đã có thể thử vào website với scheme https://.
Certificate mà Let’s Encrypt cấp cho bạn chỉ có thời hạn 3 tháng. Trước đây, khi gần đến thời hạn 3 tháng, bạn phải gọi lại sudo certbot renew để xin cấp lại certificate mới và phải khởi động lại Nginx. Giờ đây với Nginx plugin thì certbot sẽ tự động renew cho bạn và bạn không cần phải làm gì nữa cả sau khi hoàn tất bước này.
Thêm: Xem log và thông báo lỗi của app
Ngoài lệnh systemctl status để biết trạng thái chạy của app, với các chương trình chạy bằng systemd, bạn có thể xem log (cả console log và thông báo lỗi) của chúng bằng lệnh journalctl.
Sau đây là một số lệnh tôi thường dùng để xem lại console log của app trong lúc đang chạy:
# Xem lại tất cả các log output của app
# Bạn còn nhớ `SyslogIdentifier` ở trên?
# Đặt chuỗi đó sau tham số -u để chỉ hiển thị log cho my-node-app
sudo journalctl -u my-node-app
# Nếu log quá nhiều, bạn có thể nhảy dòng bằng các phím tắt của vim# Hiển thị 100 dòng log gần nhất
sudo journalctl -n 100 -u my-node-app# Hiển thị log của app trong khoảng thời gian chỉ định
sudo journalctl -u my-node-app –since “2018-09-20” –until “2018-09-26 03:00”# Hiển thị log gần đây nhất của app, sau đó tiếp tục chờ để
# hiển thị các log tiếp theo khi app đang chạy
# `-f` là tiếp tục chờ,
# `-o cat` là hiển thị log không có timestamp và id phía trước
sudo journalctl -f -o cat -u my-node-app
Lời kết
Vậy là chúng ta đã hoàn tất cài đặt một Node Web app lên Ubuntu VPS với cách thức tối ưu nhất và tiết kiệm tài nguyên nhất. Với cấu hình này, bạn có thể chạy nhiều web app trên cùng VPS, bằng cách chạy app thứ hai trên một port mới (VD: 3001) và thêm một web host ảo trong Nginx config như ở trên, với server_name là tên domain cho web app thứ hai…
Tuy nhiên đây là cách cài đặt gắn chặt với môi trường của HĐH Ubuntu. Nói như vậy để phân biệt với một số cách cài đặt sản phẩm phần mềm được cho là hiện đại hơn, sử dụng container, mà phổ biến nhất là Docker. Theo tôi việc deploy sản phẩm bằng container sẽ giúp quy trình CI[^4] được thực hiện dễ dàng hơn, không còn phụ thuộc vào Linux distro, và developer có thể giao việc deploy hoàn toàn cho devops mà không phải bận tâm. Tuy nhiên nó đòi hỏi developer phải có thêm kiến thức khá sâu về devops cũng như cách sử dụng docker ở vai trò người tạo.
Docker vẫn là một thứ khá mới mẻ đối với tôi và tôi vẫn đang nghiên cứu cách thức triển khai sản phẩm Node app trên Docker. Nếu nó thật sự hiệu quả và dễ tiếp cận, tôi sẽ viết tiếp chủ đề này với container.