Hướng dẫn cài đặt và cấu hình Jenkins trên CentOS Stream 9: Xây dựng máy chủ CI/CD tự động hóa với Java 17, Nginx và SELinux

CentOS tutorial - IT technology blog
CentOS tutorial - IT technology blog

Vấn đề thực tế: Jenkins không chạy được dù cài đúng theo docs

Tháng trước mình vừa hoàn thành dựng hệ thống CI/CD cho team dev 8 người. Chọn CentOS Stream 9 vì công ty đang chuẩn hóa toàn bộ server về dòng RHEL-compatible — rồi mới té ngửa ra rằng “cài Jenkins” không đơn giản như copy-paste từ tài liệu chính thức.

Follow đúng Jenkins docs, chạy lệnh cài xong, service start báo active, nhưng mở trình duyệt lên thì:

curl http://localhost:8080
# curl: (7) Failed to connect to localhost port 8080: Connection refused

Log Jenkins đầy những dòng kiểu này:

Nov 08 10:23:41 server jenkins[12345]: java.io.IOException: Failed to bind to 0.0.0.0/0.0.0.0:8080

Rồi thêm cái Nginx reverse proxy phía trước cũng không forward được request dù config nhìn đúng hoàn toàn. Ba vấn đề chồng lên nhau mà docs chính thức không đề cập đủ.

Phân tích nguyên nhân

Sau khoảng 2 tiếng debug, mình xác định được 3 nguyên nhân chính:

1. Java version không tương thích

Jenkins LTS 2.426+ yêu cầu Java 17 hoặc Java 21. CentOS Stream 9 mặc định pull java-11-openjdk từ repo. Mình đã cài đúng Java 17 nhưng system vẫn dùng Java 11 làm default vì chưa chạy alternatives.

# Kiểm tra Java đang active
java -version
# openjdk version "11.0.21" — sai!

# Xem tất cả Java version đang có
alternatives --list | grep java

2. SELinux chặn Jenkins bind port và đọc/ghi file

CentOS Stream 9 mặc định SELinux ở chế độ enforcing. Jenkins cần bind port 8080 và đọc/ghi vào thư mục home — tất cả đều bị deny nếu chưa cấu hình đúng context và port type.

# Xem SELinux denials liên quan Jenkins
grep "avc: denied" /var/log/audit/audit.log | grep jenkins | tail -20

3. Nginx không được phép kết nối ngược lên upstream

Nginx forward request về http://localhost:8080 nhưng SELinux boolean httpd_can_network_connect mặc định tắt — Nginx không được tạo outbound connection dù đang trên cùng một máy.

Các cách giải quyết

Cách 1: Tắt SELinux — nhanh nhưng nguy hiểm

Nhiều bài hướng dẫn trên mạng giải quyết bằng cách set SELINUX=permissive trong /etc/selinux/config. Nhanh thật, nhưng đây là sai lầm nghiêm trọng trên production. Jenkins chạy build script từ developer — bỏ SELinux là cho phép bất kỳ script nào leo quyền tùy ý trên server.

Cách 2: Chạy Jenkins trong Docker

Isolate hoàn toàn, không lo SELinux của host. Nhưng thực tế mình đã thấy 2-3 team gặp bài toán Docker-in-Docker khi build image có mount volume — debug mệt hơn cả cấu hình SELinux. Thêm một lớp phức tạp không cần thiết nếu chỉ cần Jenkins bare-metal cơ bản.

Cách 3: Cấu hình đúng SELinux + Nginx từ đầu

Mất khoảng 30 phút setup nhưng bền vững. Máy chủ của mình chạy ổn định 6 tháng nay không có issue nào liên quan hạ tầng.

Cách tốt nhất: Hướng dẫn từng bước

Bước 1: Chuẩn bị hệ thống

sudo dnf update -y
sudo dnf install -y wget curl policycoreutils-python-utils

Bước 2: Cài Java 17 và set làm default

Bước này hay bị bỏ sót phần “set default” nhất — khiến Jenkins vẫn dùng Java 11 cũ dù đã cài Java 17 đủ rồi.

# Cài Java 17
sudo dnf install -y java-17-openjdk java-17-openjdk-devel

# Chọn Java 17 làm default (interactive — chọn số tương ứng)
sudo alternatives --config java

# Verify
java -version
# openjdk version "17.x.x" — đúng rồi

Bước 3: Cài Jenkins LTS

# Thêm Jenkins repo
sudo wget -O /etc/yum.repos.d/jenkins.repo \
  https://pkg.jenkins.io/redhat-stable/jenkins.repo

# Import GPG key
sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io-2023.key

# Cài Jenkins
sudo dnf install -y jenkins

# Enable và start
sudo systemctl enable jenkins
sudo systemctl start jenkins
sudo systemctl status jenkins

Bước 4: Mở firewall tạm thời để test

# Mở port 8080 tạm thời — sẽ đóng lại sau khi có Nginx
sudo firewall-cmd --permanent --add-port=8080/tcp
sudo firewall-cmd --reload

Bước 5: Xử lý SELinux cho Jenkins

Phần quan trọng nhất mà tài liệu chính thức bỏ qua hoàn toàn.

# Xác nhận SELinux đang enforcing
getenforce
# Enforcing

# Jenkins cần bind port 8080 — thêm vào http_port_t
sudo semanage port -a -t http_port_t -p tcp 8080

# Nếu báo lỗi "already defined", kiểm tra type hiện tại
sudo semanage port -l | grep 8080

# Nếu port đang là type khác (ví dụ webcache_port_t), sửa lại
sudo semanage port -m -t http_port_t -p tcp 8080

# Check context của Jenkins home — RPM cài đúng sẽ tự gán
ls -laZ /var/lib/jenkins

Nếu bạn thay đổi JENKINS_HOME sang thư mục khác, phải relabel tay:

sudo semanage fcontext -a -t jenkins_home_t "/custom/jenkins/home(/.*)?" 
sudo restorecon -Rv /custom/jenkins/home

Bước 6: Cài và cấu hình Nginx reverse proxy

sudo dnf install -y nginx
sudo nano /etc/nginx/conf.d/jenkins.conf

Nội dung file /etc/nginx/conf.d/jenkins.conf:

upstream jenkins {
  server 127.0.0.1:8080 fail_timeout=0;
}

server {
  listen 80;
  server_name jenkins.yourdomain.com;

  location / {
    proxy_pass         http://jenkins;
    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;
    proxy_set_header   X-Forwarded-Host  $host:$server_port;

    # Upload size cho artifact
    client_max_body_size 100m;
  }
}

Bước 7: Bật SELinux boolean cho Nginx proxy

Lý do Nginx không forward được dù config đúng — boolean này mặc định tắt:

# Bật cho phép Nginx kết nối tới upstream
sudo setsebool -P httpd_can_network_connect 1

# Verify
getsebool httpd_can_network_connect
# httpd_can_network_connect --> on
# Start Nginx
sudo systemctl enable nginx
sudo systemctl start nginx

# Mở port 80
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --reload

# Đóng port 8080 lại — không cần expose nữa
sudo firewall-cmd --permanent --remove-port=8080/tcp
sudo firewall-cmd --reload

Bước 8: Buộc Jenkins chỉ listen trên localhost

Sau khi có Nginx, Jenkins không cần expose ra ngoài nữa. Thêm vào /etc/sysconfig/jenkins:

sudo nano /etc/sysconfig/jenkins
JENKINS_ARGS="--httpListenAddress=127.0.0.1"
JAVA_OPTS="-Xmx2g -Xms512m"
sudo systemctl restart jenkins

Bước 9: Unlock Jenkins lần đầu

# Lấy initial admin password
sudo cat /var/lib/jenkins/secrets/initialAdminPassword

Mở trình duyệt vào http://jenkins.yourdomain.com, nhập password trên, cài suggested plugins, tạo admin user. Tiếp theo vào Manage Jenkins → System → Jenkins URL và điền đúng domain — sai chỗ này là webhook GitHub/GitLab không hoạt động, debug mất cả buổi.

Những điều thực tế mình học sau 6 tháng chạy production

Công ty mình vẫn còn vài con server CentOS 7, và việc migrate sang AlmaLinux là bài toán mình đã xử lý rồi. Với máy chủ Jenkins mới thì mình chọn CentOS Stream 9 — vì đây là upstream trực tiếp của RHEL 9. Những gì chạy ổn ở Stream 9 gần như chắc chắn pass ở RHEL 9 khi upgrade sau, giảm rủi ro chuẩn hóa hạ tầng đáng kể.

Một số điểm thực tế sau 6 tháng vận hành:

  • Backup JENKINS_HOME thường xuyên: Toàn bộ job config, build history, credentials đều ở /var/lib/jenkins. Mình dùng cron job rsync sang S3-compatible storage mỗi đêm. Mất thư mục này là mất cả CI/CD setup.
  • Audit log trước khi debug code: Khi build job lạ bị fail, check grep "avc: denied" /var/log/audit/audit.log | tail -30 trước. Đã vài lần mình tốn cả tiếng mà nguyên nhân chỉ là SELinux deny một thao tác file nhỏ.
  • Giới hạn JVM heap: Trên server 4GB RAM trở xuống, mặc định Jenkins có thể ăn gần hết. Dòng JAVA_OPTS="-Xmx2g -Xms512m" trong bước 8 không phải optional.
  • Không để Jenkins expose port 8080 ra internet: Nginx phía trước + Jenkins chỉ bind localhost là pattern chuẩn. Jenkins không có rate limiting tốt — để lộ 8080 là mời brute force.

Setup này đang chạy ngon cho team 8 người, trung bình 60-80 build mỗi ngày, không có downtime trong 6 tháng qua. SELinux enforcing, Nginx phía trước — không phải overhead lãng phí, mà là hàng rào cần thiết cho công cụ có quyền chạy code tùy ý như Jenkins.

Share: