🚀 Triển Khai Ứng Dụng Không Gián Đoạn với Jenkins, Docker & Hostinger VPS (Gradle + React + MySQL)
Hackathon là một sự kiện thú vị nhưng cũng đầy căng thẳng - đặc biệt là khi trang web của bạn cần phải hoạt động liên tục cho đến khi kết thúc phần chấm điểm. Trong bài viết này, chúng tôi sẽ cung cấp cho bạn một hướng dẫn triển khai từng bước không thể thiếu mà bạn có thể sử dụng ngay trong kho lưu trữ của mình.
Nội dung chính
- Backend sử dụng Gradle (Spring Boot)
- Frontend sử dụng React (Nginx)
- Cơ sở dữ liệu MySQL
- Xây dựng Docker đa giai đoạn
- CI/CD với Jenkins
- VPS Hostinger (Ubuntu)
- Chiến lược phát hành kiểu Blue/Green (hoán đổi nguyên tử + hoàn tác dựa trên sức khỏe)
Kết thúc quá trình này, bạn sẽ có một trang web hoạt động, được SSL bảo vệ với thời gian ngừng hoạt động ngắn, khả năng hoàn tác an toàn và quy trình xây dựng có thể tái tạo.
🔑 Điểm Độc Đáo Trong Cách Tiếp Cận Này
- Thẻ hình ảnh xác định →
YYYYmmddHHMM-<gitshort>(ví dụ:202509041530-1a2b3c4). - Hoán đổi phát hành nguyên tử → Jenkins triển khai vào
/opt/chattingo/releases/<TAG>, sau đó hoán đổi nguyên tử sang/opt/chattingo/current. - Hoàn tác dựa trên sức khỏe → Nếu phiên bản mới không đạt
/actuator/healthtrong 60 giây, quá trình hoàn tác sẽ tự động kích hoạt. - Frontend phá vỡ bộ đệm → Xây dựng Docker sẽ chèn một
BUILD_IDduy nhất vàoindex.html. - Thời gian ngừng hoạt động tối thiểu → Người dùng sẽ không thấy ứng dụng bị hỏng.
- Có thể tái tạo & minh bạch → Tất cả các tập lệnh, cấu hình và Jenkinsfile đều có trong kho lưu trữ.
🛠️ Yêu Cầu Cần Thiết
Trước khi bắt đầu, hãy điền vào các trường giả định này bằng giá trị của bạn:
YOUR_DOCKERHUB_USER→ Tên người dùng DockerHubYOUR_DOMAIN→ Tên miền bạn sở hữu (cho SSL)YOUR_VPS_IP→ Địa chỉ IP của VPS HostingerSSH_USER→ Người dùng VPS (ví dụ:ubuntu)GIT_BRANCH→ Nhánh sử dụng cho triển khai (ví dụ:devops-implementation)JENKINS_CRED_IDS→ Thông tin xác thực Jenkins (dockerhub-user/pass,vps-ssh)
🏗️ Thiết Lập Docker & Docker Compose
1. Dockerfile cho Frontend
Chèn ID xây dựng để phá vỡ bộ đệm:
dockerfile
FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
ARG BUILD_ID
RUN if [ -f public/index.html ]; then sed -i "s/__BUILD_ID__/${BUILD_ID}/g" public/index.html || true; fi
RUN npm run build
FROM nginx:stable-alpine
COPY --from=build /app/build /usr/share/nginx/html
COPY nginx.frontend.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
➡️ Thêm __BUILD_ID__ vào public/index.html của bạn để tránh bộ đệm cũ.
2. Dockerfile cho Backend (Đa giai đoạn Gradle)
dockerfile
FROM gradle:8.3-jdk17 AS builder
WORKDIR /app
COPY build.gradle settings.gradle ./
COPY gradle ./gradle
COPY src ./src
RUN gradle clean build -x test --no-daemon
FROM eclipse-temurin:17-jre-jammy
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
EXPOSE 8080
HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=5 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java","-jar","/app/app.jar"]
➡️ Không cần cài đặt Gradle trên Jenkins - quá trình xây dựng diễn ra bên trong container.
3. Tệp docker-compose.yml (mẫu phát hành)
yaml
version: "3.8"
services:
db:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpass}
MYSQL_DATABASE: ${MYSQL_DATABASE:-chattingo}
volumes:
- db_data:/var/lib/mysql
restart: unless-stopped
backend:
image: YOUR_DOCKERHUB_USER/chattingo-backend:PLACEHOLDER_TAG
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/${MYSQL_DATABASE}
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: ${MYSQL_ROOT_PASSWORD}
depends_on:
- db
ports:
- "8080:8080"
restart: unless-stopped
frontend:
image: YOUR_DOCKERHUB_USER/chattingo-frontend:PLACEHOLDER_TAG
depends_on:
- backend
ports:
- "3001:80"
restart: unless-stopped
volumes:
db_data:
➡️ Jenkins sẽ thay thế PLACEHOLDER_TAG bằng $TAG duy nhất của bạn.
🔄 Pipeline Jenkins (CI/CD)
Tệp Jenkinsfile của bạn:
groovy
pipeline {
agent any
environment {
IMAGE_PREFIX = "YOUR_DOCKERHUB_USER"
VPS_SSH = "vps-ssh"
BRANCH = "GIT_BRANCH"
}
stages {
stage('Checkout') { steps { checkout scm } }
stage('Set TAG') {
steps {
script {
env.TAG = sh(
script: "echo $(date +%Y%m%d%H%M)-$(git rev-parse --short HEAD)",
returnStdout: true
).trim()
}
}
}
stage('Build Frontend Image') {
steps {
sh "docker build --build-arg BUILD_ID=${TAG} -t ${IMAGE_PREFIX}/chattingo-frontend:${TAG} ./frontend"
}
}
stage('Build Backend Image') {
steps {
sh "docker build -t ${IMAGE_PREFIX}/chattingo-backend:${TAG} ./backend"
}
}
stage('Push Images') {
steps {
withCredentials([usernamePassword(credentialsId: 'dockerhub-creds', usernameVariable: 'USER', passwordVariable: 'PASS')]) {
sh '''
echo $PASS | docker login -u $USER --password-stdin
docker push ${IMAGE_PREFIX}/chattingo-frontend:${TAG}
docker push ${IMAGE_PREFIX}/chattingo-backend:${TAG}
'''
}
}
}
stage('Deploy to VPS') {
steps {
sshagent (credentials: [env.VPS_SSH]) {
sh '''
scp docker-compose.yml ${SSH_USER}@${YOUR_VPS_IP}:/opt/chattingo/releases/${TAG}/docker-compose.yml
ssh ${SSH_USER}@${YOUR_VPS_IP} "sudo /usr/local/bin/deploy_release.sh ${TAG}"
'''
}
}
}
}
}
🟢 Tập Lệnh Triển Khai Nguyên Tử (trên VPS)
Lưu dưới dạng /usr/local/bin/deploy_release.sh:
bash
#!/usr/bin/env bash
set -euo pipefail
TAG="$1"
RELEASE_DIR="/opt/chattingo/releases/${TAG}"
CURRENT_DIR="/opt/chattingo/current"
PREV_DIR="/opt/chattingo/previous"
TIMEOUT=60
HEALTH_URL="https://YOUR_DOMAIN/actuator/health"
if [ ! -d "${RELEASE_DIR}" ]; then exit 2; fi
# Logic hoán đổi
[ -d "${CURRENT_DIR}" ] && mv "${CURRENT_DIR}" "${PREV_DIR}"
cp -r "${RELEASE_DIR}" "${CURRENT_DIR}"
cd "${CURRENT_DIR}"
docker compose up -d --remove-orphans
# Kiểm tra sức khỏe
SECONDS=0
until curl -fsS "${HEALTH_URL}" >/dev/null; do
sleep 5
[ $SECONDS -ge $TIMEOUT ] && {
docker compose down || true
[ -d "${PREV_DIR}" ] && mv "${PREV_DIR}" "${CURRENT_DIR}" && cd "${CURRENT_DIR}" && docker compose up -d
exit 3
}
done
rm -rf "${PREV_DIR}"
➡️ Điều này đảm bảo hoàn tác ngay lập tức nếu trang web của bạn không vượt qua kiểm tra sức khỏe.
🌐 Nginx Reverse Proxy
Đặt tại /etc/nginx/sites-available/chattingo:
nginx
server {
listen 80;
server_name YOUR_DOMAIN;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name YOUR_DOMAIN;
ssl_certificate /etc/letsencrypt/live/YOUR_DOMAIN/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/YOUR_DOMAIN/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3001/;
}
location /api/ {
proxy_pass http://127.0.0.1:8080/api/;
}
}
Sau đó:
bash
sudo ln -s /etc/nginx/sites-available/chattingo /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
✅ Lời Kết
✨ Cảm ơn bạn đã đọc! Nếu hướng dẫn này hữu ích cho bạn,
👉 Hãy yêu thích ❤️ bài viết để ủng hộ công việc của tôi
👉 Bình luận 💬 ý kiến, câu hỏi hoặc cải tiến của bạn
👉 Chia sẻ 🔗 với bạn bè hoặc đồng đội đang làm việc về CI/CD hoặc dự án hackathon
Hãy cùng nhau tiếp tục xây dựng và học hỏi 🚀