Khi deploy Node.js lên Fedora, SELinux chào đón trước tiên
Mình dùng Fedora làm máy development chính đã 2 năm và khá hài lòng với tốc độ cập nhật package. Nhưng lần đầu deploy Node.js app lên Fedora Server cho môi trường production, thứ mình đụng phải không phải là Node.js hay PM2 — mà là SELinux. App chạy ngon trên Ubuntu test trước đó, nhưng lên Fedora thì curl http://server-ip:3000 timeout hoàn toàn, log trắng tinh, không có error message nào rõ ràng.
Nếu bạn đang setup Fedora Server cho production, đừng tắt SELinux — dù nhiều “quick fix” trên StackOverflow gợi ý thế. Bài này đi thẳng vào từng bước: cài Node.js, PM2, rồi xử lý SELinux lẫn firewalld đúng cách. Phần SELinux hay bị bỏ qua nhất vì hầu hết tutorial chạy demo trên Ubuntu/Debian — nơi SELinux không bật mặc định.
Khái niệm cần nắm trước khi bắt tay
PM2 — Tại sao không dùng node app.js thẳng?
node app.js chạy được nhưng không phải production setup. PM2 là process manager dành riêng cho Node.js, giải quyết 3 vấn đề thực tế:
- Auto-restart: App crash thì tự khởi động lại, không cần canh màn hình lúc 3 giờ sáng
- Cluster mode: Trên server 4 core, PM2 chạy 4 instance song song — throughput tăng ~3.5x so với single process vì Node.js single-threaded
- Log management: Tập trung log, rotate tự động, không tràn đĩa
SELinux context và port labeling
SELinux gán label (context) cho từng file, process và port trong hệ thống. Port 3000 không nằm trong danh sách mặc định — kernel chặn mọi kết nối vào dù app đang lắng nghe đúng địa chỉ. Không có error nào, request chỉ timeout im lặng. Cần thêm label http_port_t cho cổng ứng dụng.
firewalld zones
Fedora dùng firewalld thay vì iptables trực tiếp. Để mở port cần thêm vào zone public hoặc zone của interface đang dùng, và phải dùng flag --permanent kèm --reload để thay đổi không mất sau reboot.
Thực hành chi tiết từng bước
Bước 1: Cài Node.js
Fedora có Node.js trong repo mặc định nhưng thường là bản cũ. Dùng NodeSource repo để lấy LTS mới nhất:
# Thêm NodeSource repo cho Node.js 20 LTS
curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -
sudo dnf install -y nodejs
# Kiểm tra
node --version # v20.x.x
npm --version
Hoặc dùng dnf module nếu muốn giữ trong ecosystem Fedora:
# Xem các stream Node.js có sẵn
sudo dnf module list nodejs
# Enable stream 20 và cài đặt
sudo dnf module enable nodejs:20 -y
sudo dnf install nodejs -y
Bước 2: Cài PM2
sudo npm install -g pm2
pm2 --version
Bước 3: Deploy ứng dụng mẫu
Tạo app đơn giản để test toàn bộ luồng:
sudo mkdir -p /var/www/myapp
sudo chown $USER:$USER /var/www/myapp
cd /var/www/myapp
// /var/www/myapp/app.js
const http = require('http');
const PORT = process.env.PORT || 3000;
const server = http.createServer((req, res) => {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello from Fedora Server!\n');
});
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
cd /var/www/myapp
pm2 start app.js --name "myapp"
pm2 status
PM2 báo online nhưng curl http://server-ip:3000 từ ngoài vẫn timeout — đây là lúc xử lý SELinux và firewalld.
Bước 4: Xử lý SELinux
Kiểm tra SELinux có đang chặn không:
sudo ausearch -m avc -ts recent | grep node
# Hoặc
sudo tail -50 /var/log/audit/audit.log | grep denied
Thấy dòng denied là đúng hướng — ít nhất biết được nguyên nhân thay vì mò mẫm. Fix như sau:
# Cài policycoreutils nếu chưa có
sudo dnf install -y policycoreutils-python-utils
# Xem port nào đang được phép HTTP
sudo semanage port -l | grep http_port_t
# Thêm port 3000 vào http_port_t
sudo semanage port -a -t http_port_t -p tcp 3000
# Kiểm tra lại
sudo semanage port -l | grep 3000
Nếu app cần ghi file (upload, cache, temp files…), thư mục đó cần SELinux context cho phép ghi:
# Đặt context ghi cho thư mục uploads
sudo semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/myapp/uploads(/.*)?"
sudo restorecon -Rv /var/www/myapp/uploads/
App cần gọi external API hoặc connect database trên host khác? Boolean này bắt buộc phải bật:
# Cho phép Node.js connect ra ngoài qua network
sudo setsebool -P httpd_can_network_connect 1
Flag -P để persist qua reboot. Thiếu cái này thì app connect MongoDB hay gọi third-party API đều fail lặng lẽ.
Bước 5: Mở cổng với firewalld
# Kiểm tra zone đang active
sudo firewall-cmd --get-active-zones
# Mở port 3000
sudo firewall-cmd --permanent --add-port=3000/tcp
sudo firewall-cmd --reload
# Kiểm tra
sudo firewall-cmd --list-ports
Nếu đặt Nginx làm reverse proxy phía trước (khuyến nghị cho production), chỉ mở 80/443 và giữ port app nội bộ:
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
Bước 6: PM2 khởi động cùng systemd
Bước này nhiều người hay quên — server reboot thì app phải tự chạy lại mà không cần SSH vào bật tay:
# Generate startup script
pm2 startup systemd
# Lệnh trên in ra 1 lệnh sudo, copy và chạy lệnh đó, ví dụ:
sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u myuser --hp /home/myuser
# Lưu danh sách process hiện tại
pm2 save
# Kiểm tra service
sudo systemctl status pm2-myuser
Bước 7: Ecosystem file cho cấu hình production
Thay vì nhớ các flag dài, dùng ecosystem file để version control cấu hình cùng code:
// /var/www/myapp/ecosystem.config.js
module.exports = {
apps: [{
name: 'myapp',
script: './app.js',
instances: 'max', // Dùng hết CPU cores
exec_mode: 'cluster', // Cluster mode cho high availability
env: {
NODE_ENV: 'production',
PORT: 3000
},
error_file: '/var/log/myapp/error.log',
out_file: '/var/log/myapp/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss'
}]
};
# Khởi động với ecosystem file
pm2 start ecosystem.config.js
# Reload zero-downtime (không mất request đang xử lý)
pm2 reload myapp
# Monitor real-time
pm2 monit
Kết luận
Phần cài Node.js và PM2 xong nhanh. Phần tốn thời gian là SELinux và firewalld — hai thứ mà hầu hết tutorial Ubuntu/Debian không đề cập. Thiếu một trong ba lệnh dưới đây, app chạy tốt local nhưng từ chối kết nối từ bên ngoài, hoặc tệ hơn: HTTP vào được nhưng database connect không được:
semanage port -a -t http_port_t -p tcp <port>— cho SELinux phép nhận kết nối vào cổng đósetsebool -P httpd_can_network_connect 1— cho app connect ra ngoàifirewall-cmd --permanent --add-port=<port>/tcp && firewall-cmd --reload— mở cổng firewall
Với PM2, pm2 startup và pm2 save là bước không được bỏ — server reboot mà app không tự chạy lại thì buổi sáng hôm sau mới biết. Đặt Nginx làm reverse proxy phía trước để giữ port app nội bộ, không expose thẳng ra internet.

