0

Hướng dẫn Setup Jenkins cho hệ thống Monolith - Bài 26: Triển khai Monolith dưới dạng Docker Container sử dụng Jenkins Pipeline

Ở Bài 25, chúng ta đã đóng gói thành công ứng dụng Monolith thành một Docker Image bất biến ([registry.yourcompany.com/monolith/core-app:v1.2.0-](https://registry.yourcompany.com/monolith/core-app:v1.2.0-)...) và đẩy lên Private Registry an toàn.

Bây giờ, tại Bài 26, chúng ta sẽ tiến hành hiện thực hóa nửa phần sau của quy trình: Viết thêm Stage triển khai (Deploy Stage) vào Jenkinsfile để Jenkins kết nối tới máy chủ Staging/Production qua SSH, tự động thực hiện lệnh docker pull bản image mới nhất, dừng container cũ, và khởi động container mới một cách mượt mà.

1. Tư duy kiến trúc: Triển khai Docker Monolith trực tiếp qua SSH

Khác với các hệ thống phức tạp phải dùng Kubernetes hay Docker Swarm ngay từ đầu, phần lớn các doanh nghiệp vừa và nhỏ vận hành hệ thống Monolith trên VPS hoặc Cloud Server sử dụng một kiến trúc tiêu chuẩn: Docker Engine chạy đơn lẻ trên máy chủ Linux kết hợp với Nginx Reverse Proxy làm cổng tiếp nhận traffic.

Luồng triển khai chuẩn của một bản build Docker Monolith sẽ diễn ra theo 5 bước:

  1. Login Registry: Máy chủ Production đăng nhập vào Private Docker Registry của công ty.

  2. Pull Image: Tải xuống phiên bản Docker Image mới nhất (--pull=always) vừa được Jenkins đẩy lên ở Bài 25.

  3. Stop & Remove Old Container: Dừng và xóa container phiên bản cũ đang chạy (để giải phóng cổng port và tài nguyên).

  4. Run New Container: Khởi động container mới với đầy đủ các thiết lập: truyền file .env bảo mật, mount thư mục storage ra ổ cứng máy chủ (để không mất file upload khi xóa container), và cấu hình chính sách tự động khởi động lại (restart=always).

  5. Health Check & Cleanup: Gọi API /api/health kiểm tra container đã sống sót thành công hay chưa, sau đó xóa bớt các image cũ rác trên server.

2. Thiết kế tệp Bash Script Triển khai Docker (deploy-docker.sh) trên Server đích

Giống như cách chúng ta đã làm ở Bài 23, để tránh làm rối tệp Jenkinsfile, hãy tạo một tệp script Bash chuyên dụng trên máy chủ Staging/Production (ví dụ tại /var/www/monolith-app/deploy-docker.sh):

Bash

#!/usr/bin/env bash

# ==============================================================================
# BASH SCRIPT DEPLOY DOCKER MONOLITH CHUẨN SẢN XUẤT (PRODUCTION-READY)
# Cách gọi: ./deploy-docker.sh <DOCKER_IMAGE_FULL_URL>
# ==============================================================================

set -euo pipefail

IMAGE_URL="${1:-}"
CONTAINER_NAME="monolith-core-app"
NETWORK_NAME="monolith-net"
SHARED_DIR="/var/www/monolith-app/shared"

if [ -z "${IMAGE_URL}" ]; then
    echo "❌ LỖI: Thiếu tham số Docker Image URL!"
    exit 1
fi

echo "🐳 Bắt đầu triển khai Docker Image: [${IMAGE_URL}]"

# 1. Đảm bảo Docker Network đã được tạo (giúp Monolith kết nối nội bộ với MySQL/Redis container nếu có)
docker network create "${NETWORK_NAME}" 2>/dev/null || true

# 2. Đăng nhập vào Private Registry (nếu cần thiết, hoặc cấu hình sẵn trên server)
# echo "YOUR_REGISTRY_PASSWORD" | docker login registry.yourcompany.com -u "robot-user" --password-stdin

# 3. Kéo phiên bản Image mới nhất từ Registry về máy chủ
echo "------------------------------------------------------------------"
echo "[Bước 1/5] Đang tải Docker Image mới từ Registry..."
docker pull "${IMAGE_URL}"

# 4. Kiểm tra và dừng Container cũ (nếu đang chạy)
echo "------------------------------------------------------------------"
echo "[Bước 2/5] Dừng và xóa container phiên bản cũ..."
if [ "$(docker ps -a -q -f name=^/${CONTAINER_NAME}$)" ]; then
    # Đổi tên container cũ thành tên tạm để phòng hờ trường hợp cần rollback khẩn cấp
    docker rename "${CONTAINER_NAME}" "${CONTAINER_NAME}-old" || true
    docker stop "${CONTAINER_NAME}-old" || true
fi

# 5. Khởi động Container phiên bản mới với các thông số tối ưu
echo "------------------------------------------------------------------"
echo "[Bước 3/5] Khởi động Container mới..."
docker run -d \
    --name "${CONTAINER_NAME}" \
    --restart unless-stopped \
    --network "${NETWORK_NAME}" \
    -p 8000:9000 \
    --env-file "${SHARED_DIR}/.env" \
    -v "${SHARED_DIR}/storage:/var/www/html/storage" \
    "${IMAGE_URL}"

# 6. Kiểm tra sức khỏe ứng dụng (Health Check)
echo "------------------------------------------------------------------"
echo "[Bước 4/5] Kiểm tra sức khỏe container (Health Check)..."
sleep 4

if docker ps | grep -q "${CONTAINER_NAME}"; then
    echo "🎉 Ứng dụng đã khởi động thành công!"
    # Dọn dẹp container cũ sau khi bản mới chạy ổn định
    docker rm "${CONTAINER_NAME}-old" 2>/dev/null || true
else
    echo "❌ BÁO ĐỘNG ĐỎ: Container phiên bản mới bị lỗi và đã dừng đột ngột!"
    echo "🔄 Đang tiến hành khôi phục lại container cũ..."
    docker rm "${CONTAINER_NAME}" 2>/dev/null || true
    if [ "$(docker ps -a -q -f name=^/${CONTAINER_NAME}-old$)" ]; then
        docker rename "${CONTAINER_NAME}-old" "${CONTAINER_NAME}"
        docker start "${CONTAINER_NAME}"
        echo "🔙 Đã khôi phục thành công về phiên bản trước!"
    fi
    exit 1
fi

# 7. Dọn dẹp các Docker Image mồ côi cũ trên ổ cứng server để tiết kiệm dung lượng
echo "------------------------------------------------------------------"
echo "[Bước 5/5] Dọn dẹp rác hệ thống Docker..."
docker image prune -f

echo "✅ HOÀN TẤT TRIỂN KHAI DOCKER MONOLITH THÀNH CÔNG!"

Phân quyền thực thi cho tệp script trên server:

Bash

sudo chmod +x /var/www/monolith-app/deploy-docker.sh

3. Hoàn thiện Jenkinsfile: Tích hợp Stage Deploy Docker Container

Dưới đây là tệp Jenkinsfile hoàn chỉnh từ Giai đoạn 1 đến Giai đoạn 3 (Đóng gói Docker Image -> Quét bảo mật Trivy -> Push Registry -> Kích hoạt lệnh Deploy Docker trên máy chủ đích qua SSH):

Groovy

pipeline {
    agent any

    parameters {
        choice(
            name: 'DEPLOY_ENV_OVERRIDE',
            choices: ['AUTO_BY_BRANCH', 'staging', 'production'],
            description: 'Chọn môi trường triển khai Docker Container.'
        )
    }

    options {
        buildDiscarder(logRotator(numToKeepStr: '30'))
        timeout(time: 30, unit: 'MINUTES')
        timestamps()
        disableConcurrentBuilds()
    }

    environment {
        APP_NAME        = "monolith-core-system"
        DOCKER_REGISTRY = "registry.yourcompany.com"
        DOCKER_REPO     = "${DOCKER_REGISTRY}/monolith/${APP_NAME}"
        REGISTRY_CRED   = "docker-registry-credentials"
        DOCKER_BUILDKIT = "1"

        CACHE_ROOT         = "/var/jenkins_home/.cache"
        COMPOSER_CACHE_DIR = "${CACHE_ROOT}/composer"
        SONAR_USER_HOME    = "${CACHE_ROOT}/sonar"
    }

    stages {
        stage('1. Checkout & Environment Mapping') {
            steps {
                checkout([
                    $class: 'GitSCM',
                    branches: scm.branches,
                    extensions: [
                        [$class: 'CloneOption', depth: 50, noTags: false, shallow: true, timeout: 10]
                    ],
                    userRemoteConfigs: scm.userRemoteConfigs
                ])

                script {
                    sh "mkdir -p ${COMPOSER_CACHE_DIR} ${SONAR_USER_HOME}"

                    env.GIT_SHORT_HASH = sh(script: 'git rev-parse --short=7 HEAD', returnStdout: true).trim()
                    env.LATEST_SEMVER_TAG = sh(script: 'git describe --tags --abbrev=0 2>/dev/null || echo "v1.0.0"', returnStdout: true).trim()
                    env.CLEAN_BRANCH = sh(script: 'git rev-parse --abbrev-ref HEAD', returnStdout: true).trim().replaceAll('[^a-zA-Z0-9.-]', '-')

                    def envCatalog = [
                        'staging': [
                            sshServer : 'Staging-Monolith-Server',
                            credId    : 'monolith-env-staging'
                        ],
                        'production': [
                            sshServer : 'Prod-Monolith-Server',
                            credId    : 'monolith-env-prod'
                        ]
                    ]

                    if (params.DEPLOY_ENV_OVERRIDE != 'AUTO_BY_BRANCH') {
                        env.TARGET_ENV = params.DEPLOY_ENV_OVERRIDE
                    } else if (env.CLEAN_BRANCH in ['main', 'master']) {
                        env.TARGET_ENV = 'production'
                    } else {
                        env.TARGET_ENV = 'staging'
                    }

                    def selected = envCatalog[env.TARGET_ENV]
                    env.SSH_SERVER_NAME = selected.sshServer
                    env.ENV_CRED_ID     = selected.credId

                    env.BUILD_VERSION = "${env.LATEST_SEMVER_TAG}-${env.TARGET_ENV}.b${env.BUILD_NUMBER}-${env.GIT_SHORT_HASH}"
                    env.IMAGE_FULL_URL = "${env.DOCKER_REPO}:${env.BUILD_VERSION}"

                    currentBuild.displayName = "🐳 [${env.TARGET_ENV.toUpperCase()}] ${env.BUILD_VERSION}"
                }
            }
        }

        stage('2. CI: Unit Tests & SonarQube Gate') {
            environment {
                APP_ENV = "testing"
                DB_CONNECTION = "sqlite"
                DB_DATABASE = ":memory:"
                XDEBUG_MODE = "coverage"
            }
            steps {
                sh '''
                    composer install --no-interaction --prefer-dist
                    mkdir -p reports
                    ./vendor/bin/phpunit --log-junit reports/junit.xml --coverage-clover reports/coverage-clover.xml
                '''
                script {
                    def scannerHome = tool 'SonarScanner-CLI'
                    withSonarQubeEnv('SonarQube-Main-Server') {
                        sh "${scannerHome}/bin/sonar-scanner -Dsonar.projectVersion=${env.BUILD_VERSION}"
                    }
                }
                timeout(time: 5, unit: 'MINUTES') {
                    waitForQualityGate abortPipeline: true
                }
            }
            post {
                always {
                    junit testResults: 'reports/junit.xml', allowEmptyResults: false
                }
            }
        }

        stage('3. Build & Scan Docker Image') {
            steps {
                echo "=== Đóng gói Docker Image đa tầng: ${env.IMAGE_FULL_URL} ==="
                sh """
                    docker build \
                        --pull \
                        --build-arg BUILD_VERSION=${env.BUILD_VERSION} \
                        --build-arg GIT_COMMIT=${env.GIT_SHORT_HASH} \
                        -t ${env.IMAGE_FULL_URL} \
                        -f Dockerfile .
                """
                echo "=== Quét lỗ hổng bảo mật Image với Trivy ==="
                sh """
                    trivy image \
                        --cache-dir ${CACHE_ROOT}/trivy \
                        --severity HIGH,CRITICAL \
                        --ignore-unfixed \
                        --exit-code 1 \
                        ${env.IMAGE_FULL_URL}
                """
            }
        }

        stage('4. Push to Docker Registry') {
            steps {
                withCredentials([usernamePassword(
                    credentialsId: "${env.REGISTRY_CRED}",
                    usernameVariable: 'REG_USER',
                    passwordVariable: 'REG_PASS'
                )]) {
                    sh '''
                        echo "$REG_PASS" | docker login ${DOCKER_REGISTRY} -u "$REG_USER" --password-stdin
                        docker push ${IMAGE_FULL_URL}
                        docker logout ${DOCKER_REGISTRY}
                    '''
                }
            }
        }

        // STAGE 5 (BÀI 26): ĐẨY CẤU HÌNH VÀ KÍCH HOẠT DEPLOY DOCKER CONTAINER TRÊN MÁY CHỦ ĐÍCH
        stage('5. Deploy Docker Container to Server') {
            steps {
                script {
                    if (env.TARGET_ENV == 'production') {
                        timeout(time: 15, unit: 'MINUTES') {
                            input message: "⚠️ XÁC NHẬN TRIỂN KHAI DOCKER CONTAINER LÊN PRODUCTION?",
                                  ok: "Phê duyệt & Deploy"
                        }
                    }

                    echo "=== Đang nạp cấu hình và kích hoạt deploy-docker.sh trên ${env.SSH_SERVER_NAME} ==="
                    
                    withCredentials([file(credentialsId: "${env.ENV_CRED_ID}", variable: 'SECRET_ENV_FILE')]) {
                        sh 'cp $SECRET_ENV_FILE .env.target'
                        
                        sshPublisher(
                            failOnError: true,
                            publishers: [
                                sshPublisherDesc(
                                    configName: "${env.SSH_SERVER_NAME}",
                                    verbose: true,
                                    transfers: [
                                        // 1. Đẩy file cấu hình .env vào thư mục shared bảo mật
                                        sshTransfer(
                                            sourceFiles: '.env.target',
                                            remoteDirectory: 'shared',
                                            execCommand: '''
                                                mv /var/www/monolith-app/shared/.env.target /var/www/monolith-app/shared/.env
                                                chmod 600 /var/www/monolith-app/shared/.env
                                            '''
                                        ),
                                        // 2. Kích hoạt script deploy Docker với tham số là URL của Docker Image vừa build
                                        sshTransfer(
                                            sourceFiles: '',
                                            remoteDirectory: '',
                                            execTimeout: 300000,
                                            execCommand: "/var/www/monolith-app/deploy-docker.sh ${env.IMAGE_FULL_URL}"
                                        )
                                    ]
                                )
                            ]
                        )
                    }
                }
            }
            post {
                always {
                    sh 'rm -f .env.target'
                }
            }
        }
    }

    post {
        always {
            sh '''
                rm -rf reports/ .env*
                docker rmi ${IMAGE_FULL_URL} 2>/dev/null || true
                docker image prune -f || true
            '''
        }
    }
}

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí