本文将系统性地介绍一个 PHP Laravel 项目(dbb-live-api)的现代化改造全过程,涵盖Docker 容器化改造多阶段镜像构建以及基于 Jenkins Pipeline 的 CI/CD 自动化部署。通过本方案,实现了开发、测试、生产环境的高度一致,并利用华为云 ELB 实现了生产环境的零停机滚动更新。


第一部分:PHP 项目 Docker 容器化改造

1.1 架构设计

本方案采用单容器多进程架构,在一个 Docker 容器内集成 Nginx、PHP-FPM 和 Supervisor。

+---------+     +------------------+     +----------------------------------+
|  用户   | --> | 外部 Nginx / ELB | --> |   / Docker Container             |
+---------+     +------------------+     |                                  |
                                        |  +-----------------------------+  |
                                        |  | Nginx (Web Server)          |  |
                                        |  +-----------------------------+  |
                                        |             |                    |
                                        |  +-----------------------------+  |
                                        |  | PHP-FPM (Application)       |  |
                                        |  +-----------------------------+  |
                                        |             |                    |
                                        |  +-----------------------------+  |
                                        |  | 可选:日志落盘                |  |
                                        |  | (access.log, error.log)     |  |
                                        |  +-----------------------------+  |
                                        +------------------+---------------+
                                                           |
                                       +-------------------+------------------+
                                       |                   |                  |
                                +-------------+   +-----------------+   +------------------+
                                |  MySQL      |   | Redis           |   | Kafka            |
                                | (数据库)     |   | (缓存)           |   | (消息队列)        |
                                 +-------------+   +-----------------+   +------------------+
  • 优势
    • 环境一致性:所有依赖打包在镜像中,“一次构建,到处运行”。
    • 简化部署:只需启动一个容器实例,降低运维复杂度。
    • 易于扩展:可快速复制容器实例进行水平扩展。

1.2 核心配置文件

Dockerfile (多阶段构建)
# ================================
# 第一阶段:构建依赖 & 缓存 (builder)
# ================================
FROM swr.cn-east-3.myhuaweicloud.com/bocheng-test/ubuntu22.04:v1 AS builder

# 创建应用用户
RUN groupadd -g 82 www-data && \
    useradd -r -u 82 -g www-data www-data || true

WORKDIR /var/www/html

# -------------------------------
# Step 1: 复制并安装 Composer 依赖
# (此步骤极少变更,利于 Docker 层缓存)
# -------------------------------
COPY src/composer.json src/composer.lock ./

RUN composer install \
    --optimize-autoloader \
    --no-dev \
    --no-interaction \
    --ignore-platform-req=ext-gd \
    --no-scripts \
    --no-plugins \
    --working-dir=/var/www/html

# -------------------------------
# Step 2: 复制源码并生成 Artisan 缓存
# (此步骤频繁变更,但会复用上一步的缓存)
# -------------------------------
COPY src/ ./

# 清理旧缓存,防止污染
RUN rm -f bootstrap/cache/*.php && \
    rm -rf storage/framework/cache/* && \
    rm -rf storage/framework/views/* && \
    rm -rf storage/framework/sessions/*

# 生成 Laravel 配置、路由、视图等缓存,提升性能
RUN php artisan config:cache && \
    php artisan route:cache && \
    php artisan view:cache && \
    php artisan event:cache

# 设置临时权限
RUN chown -R www-data:www-data storage bootstrap/cache && \
    chmod -R 775 storage bootstrap/cache

# ================================
# 第二阶段:精简运行时镜像 (final)
# ================================
FROM swr.cn-east-3.myhuaweicloud.com/bocheng-test/ubuntu22.04:v1 AS final

# 再次创建用户
RUN groupadd -g 82 www-data && \
    useradd -r -u 82 -g www-data www-data || true

# -------------------------------
# 复制服务配置文件
# -------------------------------
COPY php-fpm/php-fpm.conf /etc/php/8.0/fpm/php-fpm.conf
COPY php-fpm/php.ini /etc/php/8.0/fpm/php.ini
COPY php-fpm/pool.d/www.conf /etc/php/8.0/fpm/pool.d/www.conf

COPY nginx.conf /etc/nginx/nginx.conf
COPY laravel.conf /etc/nginx/conf.d/laravel.conf

COPY supervisord.conf /etc/supervisor/supervisord.conf

# -------------------------------
# 创建必要的日志和运行目录
# -------------------------------
RUN mkdir -p /run/php /var/log/php-fpm /var/log/nginx /var/log/supervisor && \
    touch /var/log/nginx/access.log /var/log/nginx/error.log && \
    chown -R www-data:www-data /var/log/php-fpm /var/log/nginx /var/log/supervisor && \
    chmod -R 755 /var/log/php-fpm /var/log/nginx

# -------------------------------
# 从 builder 阶段复制已构建好的应用
# -------------------------------
COPY --from=builder /var/www/html /var/www/html

WORKDIR /var/www/html

# 最终设置应用目录权限
RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache

EXPOSE 80 9000

# 使用 Supervisor 启动并管理 Nginx 和 PHP-FPM 进程
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
php-fpm/php-fpm.conf (PHP 主配置)
# cat php-fpm/php-fpm.conf 
include=/etc/php/8.0/fpm/pool.d/*.conf

[global]
pid = /run/php/php8.0-fpm.pid
error_log = /var/log/php-fpm/php8.0-fpm.log
log_level = notice
php-fpm/pool.d/www.conf (PHP-FPM 池配置)
[www]
; 监听本地 9000 端口
listen = 127.0.0.1:9000
listen.backlog = 65535
listen.allowed_clients = 127.0.0.1

user = www-data
group = www-data

; 动态进程管理
pm = dynamic
pm.max_children = 60
pm.start_servers = 12
pm.min_spare_servers = 8
pm.max_spare_servers = 20
; 防止内存泄漏,每个进程处理一定请求数后重启
pm.max_requests = 1000

; FPM 状态页路径
pm.status_path = /phpfpm_80_status

; 请求超时时间
request_terminate_timeout = 30
request_slowlog_timeout = 5
slowlog = /var/log/php-fpm/slow.log

; 安全限制
php_admin_value[open_basedir] = /var/www/html:/tmp:/run:/var/log
php-fpm/php.ini (PHP 主配置)
[PHP]
; 基础设置
engine = On
short_open_tag = Off
expose_php = Off
max_execution_time = 30
max_input_time = 60
memory_limit = 128M
post_max_size = 20M
upload_max_filesize = 10M

; 错误报告 (生产环境关闭显示,开启日志)
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT & ~E_WARNING & ~E_NOTICE
display_errors = Off
display_startup_errors = Off
log_errors = On

; 日期时区
date.timezone = Asia/Shanghai

; OPcache (关键性能优化)
[opcache]
opcache.enable = 1
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 30
opcache.max_accelerated_files = 20000
; 生产环境设为0,由CI/CD流程手动清除缓存
opcache.validate_timestamps = 0
opcache.revalidate_freq = 0
opcache.fast_shutdown = 1
opcache.save_comments = 0
opcache.load_comments = 0
; JIT 编译 (PHP 8+)
opcache.jit_buffer_size = 256M
opcache.jit = 1255

; 安全加固
disable_functions = pcntl_alarm,pcntl_fork,... ,exec,system,passthru,shell_exec,...
supervisord.conf (进程守护)
[supervisord]
nodaemon=true
user=root
logfile=/var/log/supervisor/supervisord.log
pidfile=/tmp/supervisord.pid
childlogdir=/var/log/supervisor
logfile_maxbytes=50MB
logfile_backups=5

; ------------------------------
; PHP-FPM 程序
; ------------------------------
[program:php-fpm]
command=php-fpm8.0 -F
autostart=true
autorestart=true
user=root
stdout_logfile=/var/log/supervisor/php-fpm-access.log
stderr_logfile=/var/log/supervisor/php-fpm-error.log

; ------------------------------
; Nginx 程序
; ------------------------------
[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
autostart=true
autorestart=true
user=root
stdout_logfile=/var/log/supervisor/nginx-access.log
stderr_logfile=/var/log/supervisor/nginx-error.log

第二部分:基于 Jenkins Pipeline 的 CI/CD 流水线

2.1 流水线核心功能

  1. 灵活部署模式
    • Deploy (Latest): 拉取最新代码,构建新镜像并推送。
    • Rollback (Select Build): 回滚到任意历史版本,无需重新构建。
  2. 安全凭据管理:使用 withCredentials 插件安全注入 Docker Registry 凭据。
  3. 滚动更新:逐台将服务器从 ELB 中“排水”,更新容器,健康检查通过后再恢复流量。
  4. 自定义权重:支持为不同服务器配置不同的 ELB 权重。

2.2 Jenkins Pipeline 脚本

pipeline {
    agent any

    environment {
        PROJECT_DIR = "/data/jenkinsdata/workspace/dbb-live-api"
        IMAGE_NAME = "bocheng-servers-qqutm3.swr-pro.myhuaweicloud.com/php-test/dbb-live-api"
        DOCKERFILE_PATH = '${PROJECT_DIR}/Dockerfile'
        SSH_USER = "root"
        HEALTH_CHECK_URL_TEMPLATE = "http://HOST_IP:8080/api/v1/ping"

        HCLOUD_REGION = "cn-east-3"
        POOL_ID = "2662cab3-ee2f-4c7d-8835-ac9a4d6ee7ec"
        TARGET_HOSTS_STR = "172.24.207.190,172.24.207.191"
    }

    parameters {
        choice(
            name: 'DEPLOY_ACTION',
            choices: ['Deploy (Latest)', 'Rollback (Select Build)'],
            description: 'Choose deployment method: deploy latest code or rollback to specified build'
        )
        string(
            name: 'ROLLBACK_BUILD_NUMBER',
            defaultValue: '',
            description: 'If rolling back, enter the build number (e.g. 42). Leave empty to use last successful build'
        )
    }

    stages {
        stage('Prepare') {
            steps {
                script {
                    echo "Current build number: ${env.BUILD_NUMBER}"
                    echo "Deploy action selected: ${params.DEPLOY_ACTION}"

                    if (params.DEPLOY_ACTION == 'Deploy (Latest)') {
                        env.BUILD_TAG = env.BUILD_NUMBER
                        echo "Preparing to deploy latest code. Image tag will be: ${env.BUILD_TAG}"

                        // 仅拉取 src 目录下的最新代码
                        dir('/data/jenkinsdata/workspace/dbb-live-api/src') {
                            sh 'git pull'
                        }
                    } else {
                        // 确定回滚的目标构建号
                        def rollbackBuild = params.ROLLBACK_BUILD_NUMBER ?: currentBuild.previousSuccessfulBuild?.number
                        if (!rollbackBuild) {
                            error("Cannot determine rollback version. Please enter a valid build number.")
                        }
                        env.BUILD_TAG = "${rollbackBuild}"
                        echo "Rolling back to build: ${env.BUILD_TAG}. No code changes will be made."
                    }
                }
            }
        }

        stage('Build and Push Docker Image') {
            when {
                expression { params.DEPLOY_ACTION == 'Deploy (Latest)' }
            }
            steps {
                script {
                    def imageTag = "${env.IMAGE_NAME}:${env.BUILD_TAG}"
                    withCredentials([usernamePassword(
                        credentialsId: '58cbd8df-79d7-4a63-9ae6-3bce3b4c74b1',
                        usernameVariable: 'SWR_USER',
                        passwordVariable: 'SWR_PASS'
                    )]) {
                        sh """
                            cd ${env.PROJECT_DIR}
                            docker build -t ${imageTag} -f ${env.DOCKERFILE_PATH} .
                            docker login -u ${SWR_USER} -p '${SWR_PASS}' bocheng-servers-qqutm3.swr-pro.myhuaweicloud.com
                            docker push ${imageTag}
                            echo "Image pushed successfully: ${imageTag}"
                        """
                    }
                }
            }
        }

        stage('Rollback - Pull Old Image') {
            when {
                expression { params.DEPLOY_ACTION == 'Rollback (Select Build)' }
            }
            steps {
                script {
                    def imageTag = "${env.IMAGE_NAME}:${env.BUILD_TAG}"
                    withCredentials([usernamePassword(
                        credentialsId: '58cbd8df-79d7-4a63-9ae6-3bce3b4c74b1',
                        usernameVariable: 'SWR_USER',
                        passwordVariable: 'SWR_PASS'
                    )]) {
                        sh """
                            docker login -u ${SWR_USER} -p '${SWR_PASS}' bocheng-servers-qqutm3.swr-pro.myhuaweicloud.com
                            docker pull ${imageTag}
                            if [ \$? -ne 0 ]; then
                                echo "Failed to pull image ${imageTag}. Check if it exists."
                                exit 1
                            fi
                            echo "Successfully pulled old image: ${imageTag}"
                        """
                    }
                }
            }
        }

        stage('Deploy to Servers (Rolling Update)') {
            steps {
                script {
                    def imageTag = "${env.IMAGE_NAME}:${env.BUILD_TAG}"
                    def hosts = env.TARGET_HOSTS_STR.split(',')

                    // 定义每台主机的 ELB 权重
                    def hostWeights = [
                        "172.24.207.191": 100,
                        "172.24.207.190": 80
                    ]

                    withCredentials([usernamePassword(
                        credentialsId: '58cbd8df-79d7-4a63-9ae6-3bce3b4c74b1',
                        usernameVariable: 'SWR_USER',
                        passwordVariable: 'SWR_PASS'
                    )]) {
                        for (host in hosts) {
                            host = host.trim()
                            echo "Starting deployment to server: ${host}"

                            // 1. 查询 ELB Member ID
                            def memberId = sh(
                                script: """
                                    /usr/local/bin/hcloud ELB ListMembers/v3 \\
                                      --cli-region=${env.HCLOUD_REGION} \\
                                      --pool_id=${env.POOL_ID} \\
                                      | jq -r '.members[] | select(.address == \"${host}\") | .id'
                                """,
                                returnStdout: true
                            ).trim()

                            if (!memberId || memberId == 'null' || memberId.isEmpty()) {
                                error "Failed to get valid member_id for host: ${host}. Got: '${memberId}'"
                            }
                            echo "Got member_id: ${memberId}"

                            // 2. 设置 ELB 权重为 0 (排水)
                            echo "Setting ELB weight of ${host} (${memberId}) to 0"
                            sh """
                                /usr/local/bin/hcloud ELB UpdateMember/v3 \\
                                  --cli-region=${env.HCLOUD_REGION} \\
                                  --pool_id=${env.POOL_ID} \\
                                  --member_id=${memberId} \\
                                  --member.weight=0
                            """

                            sleep(time: 10, unit: 'SECONDS')

                            // 3. 在目标服务器上部署新容器
                            echo "Deploying container on ${host} with image: ${imageTag}"
                            sh """
                                ssh ${env.SSH_USER}@${host} "
                                    echo '[Deployment] Logging into SWR...';
                                    docker login -u ${SWR_USER} -p '${SWR_PASS}' bocheng-servers-qqutm3.swr-pro.myhuaweicloud.com;
                                    if [ \$? -ne 0 ]; then exit 1; fi;

                                    echo '[Deployment] Removing old container...';
                                    docker rm -f live-api || true;

                                    echo '[Deployment] Starting new container...';
                                    docker run -d --name live-api -p 8080:80 ${imageTag};
                                    if [ \$? -eq 0 ]; then echo 'Container started.'; else exit 1; fi
                                "
                            """

                            // 4. 健康检查
                            def healthUrl = env.HEALTH_CHECK_URL_TEMPLATE.replace('HOST_IP', host)
                            echo "Health check URL: ${healthUrl}"
                            def maxRetries = 6
                            def retryCount = 0
                            def isHealthy = false

                            while (retryCount < maxRetries && !isHealthy) {
                                def status = sh(
                                    script: "curl -f --connect-timeout 5 --max-time 10 '${healthUrl}'",
                                    returnStatus: true,
                                    label: "Health check attempt ${retryCount + 1}"
                                )
                                if (status == 0) {
                                    isHealthy = true
                                } else {
                                    echo "Health check failed. Retrying..."
                                    sleep(time: 10, unit: 'SECONDS')
                                    retryCount++
                                }
                            }

                            if (!isHealthy) {
                                error "Health check failed after ${maxRetries} attempts for ${host}"
                            }

                            // 5. 恢复原始 ELB 权重
                            def targetWeight = hostWeights[host]
                            if (targetWeight == null) {
                                echo "Warning: No weight configured for ${host}, using default 100"
                                targetWeight = 100
                            }
                            echo "Restoring ELB weight of ${host} (${memberId}) to ${targetWeight}"
                            sh """
                                /usr/local/bin/hcloud ELB UpdateMember/v3 \\
                                  --cli-region=${env.HCLOUD_REGION} \\
                                  --pool_id=${env.POOL_ID} \\
                                  --member_id=${memberId} \\
                                  --member.weight=${targetWeight}
                            """

                            echo "Deployment to ${host} completed successfully."
                        }
                    }
                }
            }
        }
    }

    post {
        success {
            echo "Deployment succeeded. Rolling update completed."
        }
        failure {
            echo "Deployment failed. Please check logs for details."
        }
        always {
            echo "Build finished. Job: ${env.JOB_NAME}, Build Number: #${env.BUILD_NUMBER}, Result: ${currentBuild.result}"
        }
    }
}

附加:基础镜像制作

为了保证整个技术栈的稳定性和一致性,我们首先从官方 ubuntu:22.04 镜像出发,定制了一个包含 PHP 8.0、Composer、Nginx 和 Supervisor 的专用基础镜像 swr.cn-east-3.myhuaweicloud.com/bocheng-test/ubuntu22.04:v1

制作步骤摘要

  1. 拉取并启动基础容器

    docker pull ubuntu:22.04
    docker run -itd --name base ubuntu:22.04 bash
    docker exec -it base bash
    
  2. 更换国内软件源

    sed -i 's|http://archive.ubuntu.com/ubuntu/|http://mirrors.aliyun.com/ubuntu/|g' /etc/apt/sources.list
    sed -i 's|http://security.ubuntu.com/ubuntu/|http://mirrors.aliyun.com/ubuntu/|g' /etc/apt/sources.list
    apt-get update
    
  3. 添加 PHP PPA 并安装 PHP 8.0

    apt-get install -y software-properties-common
    add-apt-repository ppa:ondrej/php -y
    apt-get update
    
    apt-get install -y \
        php8.0-fpm php8.0-cli php8.0-common php8.0-curl php8.0-mbstring \
        php8.0-xml php8.0-dom php8.0-zip php8.0-pdo php8.0-mysql \
        php8.0-redis php8.0-bcmath php8.0-gd php8.0-opcache php8.0-intl \
        php8.0-tokenizer php8.0-json php8.0-fileinfo
    
  4. 配置 PHP

    # 修改主配置
    sed -i 's/memory_limit = .*/memory_limit = 256M/' /etc/php/8.0/fpm/php.ini
    sed -i 's/upload_max_filesize = .*/upload_max_filesize = 50M/' /etc/php/8.0/fpm/php.ini
    sed -i 's/post_max_size = .*/post_max_size = 100M/' /etc/php/8.0/fpm/php.ini
    sed -i 's/;date.timezone.*/date.timezone = Asia\/Shanghai/' /etc/php/8.0/fpm/php.ini
    
    # 启用并配置 OPcache
    echo "opcache.enable=1" >> /etc/php/8.0/fpm/conf.d/10-opcache.ini
    echo "opcache.memory_consumption=256" >> /etc/php/8.0/fpm/conf.d/10-opcache.ini
    echo "opcache.max_accelerated_files=20000" >> /etc/php/8.0/fpm/conf.d/10-opcache.ini
    echo "opcache.validate_timestamps=0" >> /etc/php/8.0/fpm/conf.d/10-opcache.ini
    
    # 配置 PHP-FPM 监听地址
    sed -i 's/listen = .*/listen = 127.0.0.1:9000/' /etc/php/8.0/fpm/pool.d/www.conf
    
  5. 安装其他必要组件

    # 安装 Composer
    curl -sS https://getcomposer.org/installer | php8.0 -- --version=2.0.14 --install-dir=/usr/local/bin --filename=composer
    composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
    
    # 安装 Nginx 和 Supervisor
    apt-get install -y nginx supervisor vim git unzip
    
  6. 清理与提交

    # 清理缓存
    apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
    
    # 提交为新的基础镜像
    docker commit base swr.cn-east-3.myhuaweicloud.com/bocheng-test/ubuntu22.04:v1
    

总结

通过对 dbb-live-api 项目的深度改造,我们建立了一套完整的现代化应用交付体系:

  1. 容器化:利用 Docker 多阶段构建,创建了轻量、安全、高性能的运行时镜像。
  2. 自动化:通过 Jenkins Pipeline 实现了从代码拉取、镜像构建、推送、部署到回滚的全流程自动化。
  3. 高可用:结合华为云 ELB 的滚动更新策略,确保了生产环境发布过程中的业务连续性。

该方案显著提升了团队的 DevOps 效率,为后续向 Kubernetes 等更高级编排平台演进奠定了坚实的基础。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐