Cài bản Node
Bản Node chạy như một dịch vụ thường trực: một tiến trình, một cơ sở dữ liệu SQLite nằm trên đĩa, ngoài ra không phải cài gì thêm — không cơ sở dữ liệu ngoài, không bộ nhớ đệm, không hàng đợi. Hướng dẫn này đi từ một thư mục trống đến bản cài đang chạy thật, gồm cả những phần mà phần lớn hướng dẫn bỏ qua: dịch vụ hệ thống, proxy ngược, TLS, sao lưu và nâng cấp.
Cần những gì#
| Thành phần | Phiên bản | Vì sao |
|---|---|---|
| Node.js | 22 trở lên | Sản phẩm dựa vào bộ chạy kiểm thử có sẵn và vào các giao diện mới. |
| Một trình biên dịch C | build-essential | better-sqlite3 được biên dịch lúc cài, trừ khi đã có sẵn tệp nhị phân cho nền tảng của bạn. |
| Một ổ đĩa cục bộ | — | Cho SQLite và các tệp được tải lên. Tuyệt đối không dùng ổ chia sẻ qua mạng: xem bên dưới. |
Một máy một nhân với 512 MB bộ nhớ là đủ cho vài chục người. Điều quan trọng không phải sức mạnh mà là cái đĩa: nó phải là đĩa cục bộ và phải được sao lưu.
1. Lấy về và cài#
git clone <kho-của-bạn> /var/www/toutadmin
cd /var/www/toutadmin
npm ci --omit=dev
Dùng npm ci chứ không phải npm install: nó cài đúng những gì tệp
khoá mô tả, và không bao giờ viết lại tệp ấy. Trên máy chủ, một phiên bản trượt đi là sự cố
không ai hiểu nổi.
better-sqlite3 là phụ thuộc duy nhất biên dịch mã C.
sudo apt install -y build-essential python3 gỡ được gần như mọi trường hợp.
2. Cấu hình#
cp .env.example .env
Mọi thứ đặt bằng biến môi trường — đọc từ .env, hoặc do trình quản lý dịch vụ
của bạn đặt. Không biến nào bắt buộc: không có .env thì bản cài vẫn khởi động ở
cổng 3000 và đưa bạn tới trình cài đặt.
| Biến | Mặc định | Làm gì |
|---|---|---|
PORT | 3000 | Cổng lắng nghe. |
NODE_ENV | — | Ở môi trường thật đặt production: cookie nghiêm ngặt, không in vết chi tiết. |
SESSION_SECRET | tự sinh | Niêm phong các phiên. Bỏ trống thì nó được sinh trong data/session.key. |
INSTALL_TOKEN | — | Trình cài đặt đòi trước khi cài. Nên đặt trên máy chủ phơi ra internet. |
TRUST_PROXY | — | Đặt 1 khi có proxy tin cậy phía trước, và chỉ khi ấy. |
DB_PATH | data/app.sqlite | Cơ sở dữ liệu. |
UPLOAD_DIR, CV_DIR, VAULT_DIR, SIGN_DIR, DOCS_DIR, BACKUP_DIR | nằm dưới data/ | Các thư mục tệp: ảnh, hồ sơ ứng tuyển, két, cặp ký, chứng từ nhận được, kho lưu. |
LOGIN_RATE_LIMIT | 10 | Số lần thử đăng nhập mỗi mười lăm phút, tính theo địa chỉ. |
GLOBAL_RATE_LIMIT | 300 | Số yêu cầu mỗi phút, tính theo địa chỉ. |
API_RATE_LIMIT | — | Số lần gọi API mỗi phút, tính theo mã thông báo. |
SESSION_IDLE_MINUTES | 60 | Không hoạt động quá lâu thì phiên rớt. |
SESSION_MAX_HOURS | 12 | Thời hạn tuyệt đối, không hoạt động nào kéo dài thêm được. |
ADMIN_EMAIL, ADMIN_PASSWORD | — | Cài không cần giao diện: tạo quản trị viên ngay khi khởi động. |
Đổi SESSION_SECRET là đá tất cả mọi người ra cùng lúc. Hãy đặt một lần, sinh
ngẫu nhiên
(node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"), và
lưu nó cùng phần còn lại. Đừng bao giờ để nó nằm trong kho git.
Đặt mã cài đặt#
Giữa lần khởi động đầu tiên và lúc bạn vào trình cài đặt, bản cài thuộc về ai tìm ra
nó trước: người đến trước sẽ tạo tài khoản quản trị. Vậy hãy đặt
INSTALL_TOKEN trước khi mở cổng:
node -e "console.log(require('crypto').randomBytes(16).toString('hex'))"
# rồi trong .env:
INSTALL_TOKEN=c3f1…
Trình cài đặt so nó trong thời gian không đổi — so sánh thường sẽ để lộ mã theo từng ký tự — và ghi nhật ký mỗi lần từ chối. Cài xong rồi thì trình cài đặt tự đóng lại.
Cài không cần giao diện#
Với triển khai tự động, ADMIN_EMAIL và ADMIN_PASSWORD tạo quản trị
viên ngay khi khởi động, bỏ qua trình cài đặt. Sau đó hãy gỡ chúng đi: mật khẩu nằm trong môi
trường của một dịch vụ thì ai đọc được dịch vụ ấy đều đọc được.
3. Quyền truy cập và nơi để dữ liệu#
sudo useradd --system --home /var/www/toutadmin --shell /usr/sbin/nologin toutadmin
sudo chown -R toutadmin:toutadmin /var/www/toutadmin/data
sudo chmod 750 /var/www/toutadmin/data
sudo chmod 640 /var/www/toutadmin/.env
Mã nguồn có thể để chỉ đọc; thứ duy nhất phải ghi được là data/. SQLite còn ghi
các tệp đi kèm bên cạnh (-wal, -shm): cái đáng kể là
thư mục, không riêng gì tệp cơ sở dữ liệu.
NFS và SMB nói dối về việc khoá tệp. SQLite dựa vào chính cái khoá ấy để ngăn hai bên ghi cùng lúc: trên ổ chia sẻ, cơ sở dữ liệu sẽ hỏng dần mà không báo trước. Luôn dùng đĩa cục bộ — thứ đi ra ngoài là bản sao lưu, chứ không phải cơ sở dữ liệu.
4. Dịch vụ#
Chạy bằng tay thì đóng cửa sổ lệnh là sản phẩm dừng. Hãy giao nó cho systemd:
# /etc/systemd/system/toutadmin.service
[Unit]
Description=Toutadmin
After=network.target
[Service]
Type=simple
User=toutadmin
WorkingDirectory=/var/www/toutadmin
EnvironmentFile=/var/www/toutadmin/.env
ExecStart=/usr/bin/node src/server.js
Restart=always
RestartSec=5
# Dịch vụ chỉ cần ghi vào data/.
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/www/toutadmin/data
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now toutadmin
sudo systemctl status toutadmin
journalctl -u toutadmin -f
Năm dòng siết chặt ấy không phải để trang trí: ProtectSystem=strict làm cả hệ
thống tệp không ghi được, còn ReadWritePaths mở lại đúng một thư mục buộc phải
ghi được. Một lỗ hổng cho phép ghi tuỳ ý khi ấy cũng chỉ với tới data/.
5. Proxy ngược#
Đừng bao giờ đưa cổng 3000 thẳng ra internet: nó không làm TLS, và cũng chẳng có lý do gì phải học.
server {
listen 443 ssl http2;
server_name intranet.vidu.vn;
ssl_certificate /etc/letsencrypt/live/intranet.vidu.vn/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/intranet.vidu.vn/privkey.pem;
client_max_body_size 20M; # tệp gửi vào két, chứng từ nhận được
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
server_name intranet.vidu.vn;
return 301 https://$host$request_uri;
}
Đồng thời cho dịch vụ chỉ lắng nghe trên địa chỉ vòng lặp, để cổng 3000 chỉ proxy với tới được.
TLS#
sudo certbot --nginx -d intranet.vidu.vn
Ngay khi kết nối đã mã hoá, sản phẩm tự đặt tiêu đề Strict-Transport-Security, và
không bao giờ đặt trên kết nối để trần: công bố từ một trang không mã hoá thì nó chẳng được
đọc, mà lại nhốt một lần chạy thử cục bộ trong https suốt sáu tháng.
TRUST_PROXY đi cùng proxy, và chỉ cùng proxy
Biến này khiến máy chủ tin lời địa chỉ nguồn ghi trong tiêu đề. Sau nginx thì đúng là cần
thế — nếu không, mọi yêu cầu trông như đến từ 127.0.0.1 và các hạn mức theo địa
chỉ chẳng còn bảo vệ gì. Không có proxy phía trước thì ngược lại: ai cũng khai địa chỉ tuỳ
ý và đi vòng qua hạn mức.
6. Trình cài đặt#
Mở tên miền của bạn. Chừng nào chưa có tài khoản nào, mọi địa chỉ đều chuyển về
/installation. Năm bước:
- Ngôn ngữ của bản cài, trong 16 ngôn ngữ.
- Các yêu cầu hệ thống, được kiểm và hiển thị.
- Doanh nghiệp: cái tên sẽ xuất hiện khắp nơi.
- Số ngày phép năm cấp cho mỗi nhân viên mới không làm tự do.
- Tài khoản quản trị: địa chỉ và mật khẩu ít nhất mười hai ký tự.
Ngay khi có một tài khoản, /installation chuyển về trang đăng nhập: trình cài đặt
đã tự đóng, không có tệp nào phải xoá bằng tay.
7. Vòng quét định kỳ#
Khác với bản PHP, ở đây không phải đặt lịch gì cả: máy chủ mang sẵn bộ lập lịch của nó, cứ mỗi giờ lại thức dậy và làm tám việc.
| Vòng quét làm gì | Bảo đảm |
|---|---|
| Đóng những tài khoản đã hết hợp đồng | Cũng được kiểm lúc đăng nhập và khi mở bảng điều khiển |
| Phát hành hoá đơn cho các thuê bao đến kỳ | Một chỉ mục duy nhất ngăn xuất hoá đơn hai lần |
| Biến các kỳ hạn thành thông báo | Có khoá chống trùng: một kỳ hạn chỉ nhắc một lần |
| Dọn thông báo đã đọc và nhật ký kiểm toán | Theo thời hạn lưu đã chọn |
| Lấy hộp thư kế toán về (IMAP) | Nếu việc lấy thư đã được cấu hình |
| Xả hàng đợi webhook, kể cả các lần gửi lại | Năm lần thử rồi bỏ |
| Tạo bản sao lưu tự động và gửi ra ngoài | Khi đã đến kỳ đã định |
Mỗi thao tác đều lặp lại vẫn cho cùng kết quả: chạy lại vòng quét không xuất hoá đơn hai lần, không báo hai lần. Chính nhờ vậy mà bạn có thể khởi động lại dịch vụ bất cứ lúc nào mà không phải đắn đo.
8. Kiểm tra rằng mọi thứ chạy#
# các bài kiểm thử (không ghi vào cơ sở dữ liệu của bạn)
npm test
# một bản cài trình diễn, để đi một vòng
node scripts/seed-demo.js
Rồi trong giao diện: tạo một thành viên, đăng nhập bằng người đó, gửi lên một tệp đính kèm, chạy một bản sao lưu bằng tay và kiểm nó từ màn hình Sao lưu. Bốn động tác ấy chạm tới cơ sở dữ liệu, tệp, quyền và kho lưu.
9. Nâng cấp#
# 1. sao lưu trước đã, luôn luôn — từ màn hình Sao lưu
# 2. mã nguồn
cd /var/www/toutadmin
git pull
npm ci --omit=dev
# 3. khởi động lại; cơ sở dữ liệu tự chuyển đổi khi khởi động
sudo systemctl restart toutadmin
journalctl -u toutadmin -n 30 --no-pager
Lược đồ tiến triển bằng các bước chuyển đổi lặp lại vẫn cho cùng kết quả: chạy lại bản nâng cấp
không làm hỏng gì. data/ và .env không bao giờ bị đụng tới.
Quay lại#
Đưa mã nguồn về phiên bản trước rồi khởi động lại. Các bước chuyển đổi không xoá cột: một cơ sở dữ liệu đã chuyển vẫn đọc được bằng phiên bản trước, trừ khi nhật ký thay đổi nói rõ khác đi. Nếu còn ngờ, hãy phục hồi bản lưu đã lấy ở bước 1.
10. Sao lưu cho ra sao lưu#
Kho lưu tar.gz mang theo cơ sở dữ liệu và năm thư mục tệp. Cơ sở dữ liệu được chép
bằng cơ chế sao lưu trực tuyến của SQLite, cho ra một bản chép nhất quán ngay cả khi đang có
ghi — bản PHP đạt cùng kết quả bằng VACUUM INTO. Mỗi tệp mang mã băm SHA-256 của
nó, được đối chiếu lúc phục hồi.
Một kho lưu nằm lại trên chính máy chủ mà nó bảo vệ thì chẳng bảo vệ được gì: hãy cấu hình nơi
lưu từ xa (FTPS hoặc Google Drive) từ màn hình Sao lưu, và đăng ký một webhook cho
sauvegarde.echec để biết khi việc gửi ra ngoài thất bại. Chi tiết nằm ở trang
Sao lưu và phục hồi.
Thử ở máy mình trước đã#
npm install
cp .env.example .env
npm run dev # khởi động lại sau mỗi thay đổi
Mở http://localhost:3000. Ở máy mình, hãy để NODE_ENV trống: trong
production, cookie bị đánh dấu «chỉ dùng kết nối an toàn» và sẽ không được giữ
trên kết nối để trần — bạn sẽ quẩn quanh mãi ở trang đăng nhập.
Chuyển sang bản PHP, hoặc từ đó sang đây#
Hai bản dùng chung một lược đồ — 142 bảng, 1311 cột — và cùng một
định dạng mật khẩu. Dừng bên này, chép app.sqlite cùng các thư mục tệp, khởi động
bên kia: không có bước chuyển đổi nào. Xem Hai bản.
Tài liệu Toutadmin — dựng ngày 2026-09-13. Trang độc lập, tách rời phần mềm.