PHP 项目容器化与自动化部署实践:从 Docker 改造到 Jenkins Pipeline 滚动更新
·
本文将系统性地介绍一个 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 流水线核心功能
- 灵活部署模式:
- Deploy (Latest): 拉取最新代码,构建新镜像并推送。
- Rollback (Select Build): 回滚到任意历史版本,无需重新构建。
- 安全凭据管理:使用
withCredentials插件安全注入 Docker Registry 凭据。 - 滚动更新:逐台将服务器从 ELB 中“排水”,更新容器,健康检查通过后再恢复流量。
- 自定义权重:支持为不同服务器配置不同的 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。
制作步骤摘要
-
拉取并启动基础容器:
docker pull ubuntu:22.04 docker run -itd --name base ubuntu:22.04 bash docker exec -it base bash -
更换国内软件源:
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 -
添加 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 -
配置 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 -
安装其他必要组件:
# 安装 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 -
清理与提交:
# 清理缓存 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 项目的深度改造,我们建立了一套完整的现代化应用交付体系:
- 容器化:利用 Docker 多阶段构建,创建了轻量、安全、高性能的运行时镜像。
- 自动化:通过 Jenkins Pipeline 实现了从代码拉取、镜像构建、推送、部署到回滚的全流程自动化。
- 高可用:结合华为云 ELB 的滚动更新策略,确保了生产环境发布过程中的业务连续性。
该方案显著提升了团队的 DevOps 效率,为后续向 Kubernetes 等更高级编排平台演进奠定了坚实的基础。
更多推荐
所有评论(0)