Bối cảnh — tại sao lại chọn stack này trên CentOS Stream 9?
Công ty mình vẫn còn vài con server chạy CentOS 7, và bài toán migrate sang AlmaLinux thì đã giải quyết xong. Với những dự án mới, mình chọn thẳng CentOS Stream 9 — đây là upstream của RHEL 9, package mới về sớm hơn Rocky/Alma khoảng 4–6 tuần, và Python 3.11 đã có sẵn trong default repos mà không cần thêm third-party source.
Cái đau đầu nhất khi deploy Python app lên RHEL-based distro không phải Nginx hay Gunicorn — mà là SELinux. Chạy setenforce 0 thì giải quyết được ngay, nhưng làm vậy trên server production là tự bắn vào chân. Bài này mình đi qua toàn bộ luồng deploy đúng cách: virtualenv → Gunicorn → systemd service → Nginx reverse proxy → SELinux policy — không tắt, không workaround bẩn.
Stack: CentOS Stream 9, Python 3.11, Flask (Django dùng y chang), Gunicorn 21.x, Nginx 1.24.
Cài đặt môi trường
Chuẩn bị hệ thống
Update hệ thống và cài các package cần thiết — bước này mất vài phút trên server mới:
sudo dnf update -y
sudo dnf install -y python3.11 python3.11-pip python3.11-devel nginx gcc
Tạo system user riêng để chạy app. Không bao giờ dùng root — và cũng đừng dùng account cá nhân. System user không có home directory, không có login shell, nếu app bị compromise thì attacker cũng không leo thang lên được nhiều:
sudo useradd --system --no-create-home --shell /sbin/nologin appuser
Tạo virtualenv và cài dependencies
Mình đặt app trong /opt/myapp thay vì /home — SELinux có sẵn context rules cho /opt, việc gán label sau này đơn giản hơn nhiều:
sudo mkdir -p /opt/myapp
sudo chown appuser:appuser /opt/myapp
# Tạo virtualenv
sudo -u appuser python3.11 -m venv /opt/myapp/venv
# Cài dependencies
sudo -u appuser /opt/myapp/venv/bin/pip install gunicorn flask
Tạo file app mẫu để test:
# /opt/myapp/app.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Hello from CentOS Stream 9!', 200
@app.route('/health')
def health():
return {'status': 'ok'}, 200
if __name__ == '__main__':
app.run()
Cấu hình chi tiết
systemd service cho Gunicorn
Unix socket thay vì TCP port — lựa chọn này vừa nhanh hơn (khoảng 15–20% với I/O-bound app) vừa giảm bề mặt tấn công, và dễ cấu hình SELinux label hơn:
sudo nano /etc/systemd/system/myapp.service
[Unit]
Description=Gunicorn daemon for myapp
After=network.target
[Service]
Type=notify
User=appuser
Group=appuser
WorkingDirectory=/opt/myapp
Environment="PATH=/opt/myapp/venv/bin"
EnvironmentFile=-/opt/myapp/.env
ExecStart=/opt/myapp/venv/bin/gunicorn \
--workers 3 \
--worker-class gthread \
--threads 2 \
--bind unix:/run/myapp/gunicorn.sock \
--access-logfile /var/log/myapp/access.log \
--error-logfile /var/log/myapp/error.log \
app:app
ExecReload=/bin/kill -s HUP $MAINPID
RuntimeDirectory=myapp
RuntimeDirectoryMode=0750
PrivateTmp=true
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
Cấu hình trên dùng gthread worker với 3 workers × 2 threads — tổng 6 luồng xử lý đồng thời. gthread phù hợp cho app có nhiều I/O (database query, external API call), khác với sync worker mặc định vốn chỉ xử lý 1 request mỗi worker.
Tạo thư mục log và phân quyền:
sudo mkdir -p /var/log/myapp
sudo chown appuser:appuser /var/log/myapp
Enable và start service:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myapp
Nginx reverse proxy
Cấu hình Nginx forward request tới Gunicorn qua Unix socket:
sudo nano /etc/nginx/conf.d/myapp.conf
upstream myapp_gunicorn {
server unix:/run/myapp/gunicorn.sock fail_timeout=0;
}
server {
listen 80;
server_name example.com;
access_log /var/log/nginx/myapp_access.log;
error_log /var/log/nginx/myapp_error.log;
location / {
proxy_set_header Host $http_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;
proxy_pass http://myapp_gunicorn;
proxy_read_timeout 90s;
}
location /static/ {
alias /opt/myapp/static/;
expires 30d;
}
}
sudo nginx -t && sudo systemctl enable --now nginx
Xử lý SELinux — phần không ai muốn làm nhưng buộc phải làm
Đây là chỗ mình mất nhiều thời gian nhất khi mới bắt đầu. Triệu chứng kinh điển: Nginx trả về 502 Bad Gateway, journalctl -u myapp thấy Gunicorn đang chạy bình thường, Nginx error log chỉ thấy connect() to unix:/run/myapp/gunicorn.sock failed (13: Permission denied). Đó là SELinux đang block ngầm — không phải lỗi app.
Cho phép Nginx kết nối tới socket:
# Cho phép Nginx connect đến network socket của app
sudo setsebool -P httpd_can_network_connect 1
# Label đúng context cho socket directory
sudo semanage fcontext -a -t httpd_var_run_t "/run/myapp(/.*)?"
sudo restorecon -Rv /run/myapp
Nếu app cần đọc file trong /opt/myapp:
sudo semanage fcontext -a -t httpd_exec_t "/opt/myapp/venv/bin/gunicorn"
sudo semanage fcontext -a -t httpd_sys_content_t "/opt/myapp(/.*)?"
sudo restorecon -Rv /opt/myapp
Mỗi khi gặp 502 không rõ nguyên nhân, mình chạy cặp lệnh này trước tiên:
# Xem log SELinux gần nhất
sudo ausearch -m avc -ts recent | audit2why
# Tạo policy tạm từ log để test
sudo ausearch -m avc -ts recent | audit2allow -M myapp_policy
sudo semodule -i myapp_policy.pp
Firewall
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
Kiểm tra và Monitoring
Verify stack đang chạy đúng
# Kiểm tra socket đã được tạo
ls -la /run/myapp/gunicorn.sock
# Test trực tiếp qua socket (bypass Nginx)
curl --unix-socket /run/myapp/gunicorn.sock http://localhost/health
# Test qua Nginx
curl -I http://localhost/
# Xem log realtime
journalctl -u myapp -f
Theo dõi workers và performance
Gunicorn docs khuyên tính số workers theo công thức (2 × CPU cores) + 1. Server 2 core → dùng 5 workers. Với cấu hình 3 workers × 2 threads ở trên, app xử lý được 6 request đồng thời — đủ cho hầu hết workload trừ khi site có traffic đột biến lớn.
# Kiểm tra PID và trạng thái workers
ps aux | grep gunicorn
# Reload config không downtime
sudo systemctl reload myapp
# Xem log access theo format
tail -f /var/log/myapp/access.log
Log rotation
Log app không tự xoay — nếu không cấu hình logrotate, vài tháng sau disk đầy lúc nào không hay:
sudo nano /etc/logrotate.d/myapp
/var/log/myapp/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
sharedscripts
postrotate
systemctl kill -s USR1 myapp
endscript
}
Checklist trước khi đưa lên production
- SELinux đang ở mode
enforcing(kiểm tra bằnggetenforce) - App không chạy bằng root (
ps aux | grep gunicornphải thấyappuser) - Endpoint
/healthtrả về 200 qua cả socket lẫn Nginx - Log rotation đã cấu hình
- Service tự khởi động lại khi server reboot (
systemctl is-enabled myapp) - File
.envkhông được commit lên git, chỉ có trên server
Stack Gunicorn + Nginx + systemd + SELinux này mình đang chạy ổn định trên mấy con server production, uptime liên tục hơn 6 tháng không cần can thiệp thủ công. Sau khi setup xong, toàn bộ lifecycle của app do systemd quản lý: restart tự động khi crash, log tập trung qua journalctl, không cần supervisor hay pm2. Đơn giản mà vững.
