整体架构 开发者 push / MR ↓ esc to interrupt GitLab CE(私有化 / 极狐) ↓ 触发 Pipeline GitLab Runner(Docker executor) ↓ ┌──────────────────────────────────────┐ │ stage:test→ PHPStan + PHPUnit │ │ stage: build → Docker 镜像构建 │ │ stage: push → 推送私有 Registry │ │ stage: deploy → SSH 平滑部署 │ │ stage: notify → 钉钉/企业微信通知 │ └──────────────────────────────────────┘ --- 第一步:GitLab CE 私有化部署 国内推荐两个选择: - 极狐 GitLab(JiHu):官方中文版,镜像在国内,速度快 → registry.gitlab.cn - GitLab CE 官方:用阿里云镜像加速拉取1.1docker-compose.yml(极狐版,推荐国内) version:'3.8'services: gitlab: image: registry.gitlab.cn/omnibus/gitlab-jh:latest# 极狐国内镜像container_name: gitlab restart: unless-stopped hostname: gitlab.your-company.com environment: GITLAB_OMNIBUS_CONFIG:|external_url'https://gitlab.your-company.com'gitlab_rails['time_zone']='Asia/Shanghai'# 内置 Container Registryregistry_external_url'https://registry.your-company.com'gitlab_rails['registry_enabled']=true# 邮件(可选)gitlab_rails['smtp_enable']=truegitlab_rails['smtp_address']='smtp.exmail.qq.com'gitlab_rails['smtp_port']=465gitlab_rails['smtp_user_name']='ci@your-company.com'gitlab_rails['smtp_password']='your-smtp-password'gitlab_rails['smtp_domain']='your-company.com'gitlab_rails['smtp_authentication']='login'gitlab_rails['smtp_enable_starttls_auto']=truegitlab_rails['smtp_tls']=true# 性能优化(4C8G 机器)puma['worker_processes']=2sidekiq['concurrency']=10postgresql['shared_buffers']='256MB'ports: -"80:80"-"443:443"-"22:22"volumes: - /data/gitlab/config:/etc/gitlab - /data/gitlab/logs:/var/log/gitlab - /data/gitlab/data:/var/opt/gitlab shm_size:'256m'gitlab-runner: image: registry.gitlab.cn/gitlab-org/gitlab-runner:latest# 极狐 Runnercontainer_name: gitlab-runner restart: unless-stopped volumes: - /data/gitlab-runner/config:/etc/gitlab-runner - /var/run/docker.sock:/var/run/docker.sock# Docker-outside-Dockerdepends_on: - gitlabdocker-composeup-d# 等待 GitLab 启动(约 2-3 分钟)dockerlogs-fgitlab|grep"gitlab Reconfigured"# 获取 root 初始密码dockerexecgitlabcat/etc/gitlab/initial_root_password --- 第二步:注册 GitLab Runner2.1获取 Runner 注册 Token GitLab 管理后台 → Admin Area → CI/CD → Runners → New instance runner,复制 Token。 或项目级别:项目 → Settings → CI/CD → Runners → New project runner。2.2注册 Runner(Docker executor)dockerexec-itgitlab-runner gitlab-runner register\--non-interactive\--url"https://gitlab.your-company.com"\--token"glrt-your-runner-token"\--executor"docker"\--docker-image"registry.cn-hangzhou.aliyuncs.com/hyperf/hyperf:8.1-alpine-v3.16-swoole"\--docker-volumes"/var/run/docker.sock:/var/run/docker.sock"\--docker-privileged\--description"hyperf-runner"\--tag-list"php,hyperf,docker"\--run-untaggedtrue\--lockedfalse2.3优化 Runner 配置(/data/gitlab-runner/config/config.toml) concurrent=4# 最多同时跑 4 个 Jobcheck_interval=3[[runners]]name="hyperf-runner"url="https://gitlab.your-company.com"token="glrt-your-runner-token"executor="docker"[runners.docker]image="registry.cn-hangzhou.aliyuncs.com/hyperf/hyperf:8.1-alpine-v3.16-swoole"privileged=truevolumes=["/var/run/docker.sock:/var/run/docker.sock","/data/runner-cache:/cache",# 持久化 cache"/data/composer-cache:/root/.composer"# Composer 全局缓存]# 使用阿里云镜像加速拉取基础镜像pull_policy=["if-not-present"]# 国内 DNSdns=["223.5.5.5","114.114.114.114"][runners.cache]Type="local"Path="/cache"Shared=true--- 第三步:项目配置3.1GitLab CI/CD 变量配置 项目 → Settings → CI/CD → Variables,添加以下变量(勾选 Masked/Protected): ┌─────────────────┬──────────────────┬──────────┐ │ 变量名 │ 说明 │ 类型 │ ├─────────────────┼──────────────────┼──────────┤ │ COMPOSER_AUTH │ 私有包认证 JSON │ Masked │ ├─────────────────┼──────────────────┼──────────┤ │ SSH_PRIVATE_KEY │ 部署服务器私钥 │ File │ ├─────────────────┼──────────────────┼──────────┤ │ STAGING_HOST │ 测试服务器 IP │ Variable │ ├─────────────────┼──────────────────┼──────────┤ │ PRODUCTION_HOST │ 生产服务器 IP │ Variable │ ├─────────────────┼──────────────────┼──────────┤ │ DEPLOY_USER │ 部署用户名 │ Variable │ ├─────────────────┼──────────────────┼──────────┤ │ REGISTRY_USER │ Registry 用户名 │ Variable │ ├─────────────────┼──────────────────┼──────────┤ │ REGISTRY_PASS │ Registry 密码 │ Masked │ ├─────────────────┼──────────────────┼──────────┤ │ DINGTALK_TOKEN │ 钉钉机器人 Token │ Masked │ └─────────────────┴──────────────────┴──────────┘3.2完整 .gitlab-ci.yml# .gitlab-ci.yml# ── 全局镜像(含 Swoole,用于测试阶段)────────────────────image: registry.cn-hangzhou.aliyuncs.com/hyperf/hyperf:8.1-alpine-v3.16-swoole# ── 阶段定义 ──────────────────────────────────────────────stages: - prepare -test- build - deploy - notify# ── 全局变量 ──────────────────────────────────────────────variables: APP_NAME:"hyperf-app"# 使用 GitLab 内置 Container RegistryIMAGE_NAME:"$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"IMAGE_LATEST:"$CI_REGISTRY_IMAGE:latest"# Composer 阿里云镜像COMPOSER_MIRROR:"https://mirrors.aliyun.com/composer/"# 关闭 Swoole 短名称(Hyperf 必须)PHP_INI_SCAN_DIR:""# ── 全局 before_script ────────────────────────────────────default: before_script: -echo"Asia/Shanghai">/etc/timezone - php-v-composer--version# ── Cache 策略(Composer vendor 跨 Job 复用)─────────────.composer-cache:&composer-cache cache: key: files: - composer.lock paths: - vendor/ policy: pull-push# ── SSH 部署公共配置 ──────────────────────────────────────.ssh-setup:&ssh-setup before_script: - apkadd--no-cache openssh-client -eval$(ssh-agent-s)-echo"$SSH_PRIVATE_KEY"|tr-d'\r'|ssh-add - -mkdir-p~/.ssh&&chmod700~/.ssh - ssh-keyscan-H$DEPLOY_HOST>>~/.ssh/known_hosts# ═══════════════════════════════════════════════════════════# Stage 1: prepare - 安装依赖# ═══════════════════════════════════════════════════════════composer:install: stage: prepare<<: *composer-cache script:# 配置阿里云 Composer 镜像-composerconfig-grepo.packagistcomposer$COMPOSER_MIRROR# 配置私有包认证-|if[-n"$COMPOSER_AUTH"];thenecho$COMPOSER_AUTH>/root/.composer/auth.jsonfi-composerinstall--no-interaction --no-progress --prefer-dist --optimize-autoloader artifacts: paths: - vendor/ expire_in:1hour rules: - if:'$CI_PIPELINE_SOURCE == "push"'- if:'$CI_PIPELINE_SOURCE == "merge_request_event"'# ═══════════════════════════════════════════════════════════# Stage 2: test - 代码质量 + 单元测试(并行)# ═══════════════════════════════════════════════════════════phpstan: stage:testneeds:["composer:install"]script: - php vendor/bin/phpstan analyse--level=5--no-progress --error-format=gitlab app/>phpstan-report.json||true- php vendor/bin/phpstan analyse--level=5app/ artifacts: reports: codequality: phpstan-report.json expire_in:1week rules: - if:'$CI_PIPELINE_SOURCE == "push"'- if:'$CI_PIPELINE_SOURCE == "merge_request_event"'phpunit: stage:testneeds:["composer:install"]variables: APP_ENV: testing DB_DRIVER: sqlite DB_DATABASE:":memory:"REDIS_HOST: redis# 测试阶段启动 Redis 服务services: - name: redis:7-alpine alias: redis script: - php vendor/bin/phpunit--configurationphpunit.xml --log-junit reports/junit.xml --coverage-text--colors=never coverage:'/^\s*Lines:\s*\d+.\d+\%/'artifacts: reports: junit: reports/junit.xml paths: - reports/ expire_in:1week rules: - if:'$CI_PIPELINE_SOURCE == "push"'- if:'$CI_PIPELINE_SOURCE == "merge_request_event"'# ═══════════════════════════════════════════════════════════# Stage 3: build - 构建 Docker 镜像# ═══════════════════════════════════════════════════════════docker:build: stage: build image: docker:24 services: - docker:24-dind variables: DOCKER_TLS_CERTDIR:"/certs"DOCKER_BUILDKIT:"1"needs:["phpstan","phpunit"]script:# 登录 GitLab 内置 Registry-dockerlogin-u$CI_REGISTRY_USER-p$CI_REGISTRY_PASSWORD$CI_REGISTRY# 拉取缓存层(加速构建)-dockerpull$IMAGE_LATEST||true# 多阶段构建-dockerbuild --cache-from$IMAGE_LATEST--build-argBUILDKIT_INLINE_CACHE=1--build-argAPP_ENV=production--label"git.commit=$CI_COMMIT_SHA"--label"git.branch=$CI_COMMIT_REF_NAME"--label"build.number=$CI_PIPELINE_ID"-t$IMAGE_NAME-t$IMAGE_LATEST.-dockerpush$IMAGE_NAME-dockerpush$IMAGE_LATESTrules: - if:'$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "master"'- if:'$CI_COMMIT_BRANCH =~ /^release\//'# ═══════════════════════════════════════════════════════════# Stage 4: deploy - 多环境部署# ═══════════════════════════════════════════════════════════# ── 测试环境(main 分支自动部署)────────────────────────deploy:staging: stage: deploy image: alpine:3.18 needs:["docker:build"]variables: DEPLOY_HOST:$STAGING_HOSTDEPLOY_PATH:"/data/www/hyperf-app-staging"<<: *ssh-setup script: -|ssh-oStrictHostKeyChecking=no${DEPLOY_USER}@${DEPLOY_HOST}" set -e echo '>>> 登录 Registry...' docker login -u${CI_REGISTRY_USER}-p${CI_REGISTRY_PASSWORD}${CI_REGISTRY}echo '>>> 拉取最新镜像...' docker pull${IMAGE_NAME}echo '>>> 停止旧容器...' docker stop hyperf-staging 2>/dev/null || true docker rm hyperf-staging 2>/dev/null || true echo '>>> 启动新容器...' docker run -d \ --name hyperf-staging \ --restart unless-stopped \ -p 9502:9501 \ --env-file${DEPLOY_PATH}/.env \ -v${DEPLOY_PATH}/runtime:/app/runtime \${IMAGE_NAME}echo '>>> 等待服务就绪...' sleep 5 docker exec hyperf-staging php bin/hyperf.php server:check || true echo '>>> 清理旧镜像...' docker image prune -f "environment: name: staging url: https://staging.your-company.com rules: - if:'$CI_COMMIT_BRANCH == "main"'# ── 生产环境(master 分支,人工确认)────────────────────deploy:production: stage: deploy image: alpine:3.18 needs:["docker:build"]variables: DEPLOY_HOST:$PRODUCTION_HOSTDEPLOY_PATH:"/data/www/hyperf-app"<<: *ssh-setup script: -|ssh-oStrictHostKeyChecking=no${DEPLOY_USER}@${DEPLOY_HOST}" set -e echo '>>> 备份当前版本...' BACKUP_TAG=\$(dockerinspect hyperf-app--format='{{.Config.Image}}'2>/dev/null||echo'none')echo\"当前版本: \$BACKUP_TAG\">>${DEPLOY_PATH}/deploy.log echo '>>> 登录 Registry...' docker login -u${CI_REGISTRY_USER}-p${CI_REGISTRY_PASSWORD}${CI_REGISTRY}echo '>>> 拉取新镜像...' docker pull${IMAGE_NAME}echo '>>> 平滑切换(先启新容器,再停旧容器)...' docker run -d \ --name hyperf-app-new \ --restart unless-stopped \ -p 9503:9501 \ --env-file${DEPLOY_PATH}/.env \ -v${DEPLOY_PATH}/runtime:/app/runtime \${IMAGE_NAME}sleep 8 echo '>>> 健康检查...' curl -sf http://127.0.0.1:9503/health || (docker stop hyperf-app-new && docker rm hyperf-app-new && exit 1) echo '>>> 切换 Nginx upstream...' sed -i 's/9501/9503/g' /usr/local/nginx/conf/vhost/api.your-company.com.conf /usr/local/nginx/sbin/nginx -s reload echo '>>> 停止旧容器...' docker stop hyperf-app 2>/dev/null || true docker rm hyperf-app 2>/dev/null || true echo '>>> 重命名新容器...' docker rename hyperf-app-new hyperf-app echo '>>> 恢复 Nginx upstream...' sed -i 's/9503/9501/g' /usr/local/nginx/conf/vhost/api.your-company.com.conf /usr/local/nginx/sbin/nginx -s reload echo '>>> 清理旧镜像...' docker image prune -f echo '>>> 部署完成 ✅' "environment: name: production url: https://api.your-company.com when: manual# 人工点击确认才部署allow_failure:falserules: - if:'$CI_COMMIT_BRANCH == "master"'when: manual# ── 非 Docker 部署(OneinStack 裸机,用 Supervisor)────deploy:bare-metal: stage: deploy image: alpine:3.18 needs:["phpstan","phpunit"]variables: DEPLOY_HOST:$PRODUCTION_HOSTDEPLOY_PATH:"/data/www/hyperf-app"PHP_BIN:"/usr/local/php/bin/php"<<: *ssh-setup script: -|ssh-oStrictHostKeyChecking=no${DEPLOY_USER}@${DEPLOY_HOST}" set -e cd${DEPLOY_PATH}echo '>>> 拉取代码...' git fetch origin git reset --hard origin/${CI_COMMIT_BRANCH}echo '>>> 安装依赖...'${PHP_BIN}/usr/local/bin/composer install \ --no-dev --optimize-autoloader --no-interaction echo '>>> 重建注解缓存...'${PHP_BIN}bin/hyperf.php di:init-proxy echo '>>> 平滑重启 Worker...' PID_FILE=${DEPLOY_PATH}/runtime/hyperf.pid if [ -f\"\$PID_FILE\"]; then kill -USR1 \$(cat\$PID_FILE)echo '>>> SIGUSR1 已发送,Worker 平滑重启中...' else supervisorctl restart hyperf-app fi echo '>>> 验证服务...' sleep 3 curl -sf http://127.0.0.1:9501/health && echo '>>> 健康检查通过 ✅' "environment: name: production-bare url: https://api.your-company.com when: manual rules: - if:'$CI_COMMIT_BRANCH == "master"'when: manual# ═══════════════════════════════════════════════════════════# Stage 5: notify - 钉钉通知# ═══════════════════════════════════════════════════════════notify:success: stage: notify image: alpine:3.18 needs: - job: deploy:staging optional:true- job: deploy:production optional:truescript: - apkadd--no-cachecurl-|curl-s-XPOST"https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_TOKEN}"\-H"Content-Type: application/json"\-d"{\"msgtype\":\"markdown\",\"markdown\": {\"title\":\"✅ 部署成功 -${APP_NAME}\",\"text\":\"## ✅ 部署成功\\n- **项目**:${APP_NAME}\\n- **分支**:${CI_COMMIT_REF_NAME}\\n- **提交**:${CI_COMMIT_SHORT_SHA}\\n- **提交人**:${CI_COMMIT_AUTHOR}\\n- **流水线**: [#${CI_PIPELINE_ID}](${CI_PIPELINE_URL})\"} }"rules: - if:'$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "master"'when: on_success notify:failure: stage: notify image: alpine:3.18 script: - apkadd--no-cachecurl-|curl-s-XPOST"https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_TOKEN}"\-H"Content-Type: application/json"\-d"{\"msgtype\":\"markdown\",\"markdown\": {\"title\":\"❌ 流水线失败 -${APP_NAME}\",\"text\":\"## ❌ 流水线失败\\n- **项目**:${APP_NAME}\\n- **分支**:${CI_COMMIT_REF_NAME}\\n- **失败阶段**:${CI_JOB_STAGE}\\n- **提交人**:${CI_COMMIT_AUTHOR}\\n> [查看日志](${CI_JOB_URL})\"},\"at\": {\"isAtAll\": true} }"rules: - if:'$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "master"'when: on_failure --- 第四步:Hyperf 项目适配 Dockerfile(多阶段,生产优化)# ── Stage 1: 安装依赖 ─────────────────────────────────────FROM registry.cn-hangzhou.aliyuncs.com/hyperf/hyperf:8.1-alpine-v3.16-swoole AS deps WORKDIR /app# 利用层缓存:先复制 composer 文件COPY composer.json composer.lock ./# 配置阿里云镜像 + 私有包认证ARG COMPOSER_AUTH RUNcomposerconfig-grepo.packagistcomposerhttps://mirrors.aliyun.com/composer/\&&composerinstall\--no-dev\--no-scripts\--no-autoloader\--prefer-dist\--no-interaction COPY..RUNcomposerdump-autoload--optimize\&&php bin/hyperf.php di:init-proxy# ── Stage 2: 生产镜像 ─────────────────────────────────────FROM registry.cn-hangzhou.aliyuncs.com/hyperf/hyperf:8.1-alpine-v3.16-swoole WORKDIR /app# 只复制必要文件COPY--from=deps /app.# 运行时配置RUNecho"swoole.use_shortname=Off">>/etc/php8/conf.d/swoole.ini\&&mkdir-pruntime/logs\&&chmod-R777runtime EXPOSE9501STOPSIGNAL SIGTERM HEALTHCHECK--interval=10s--timeout=5s--retries=3\CMDcurl-sfhttp://127.0.0.1:9501/health||exit1CMD["php","/app/bin/hyperf.php","start"]phpunit.xml<?xmlversion="1.0"encoding="UTF-8"?><phpunitbootstrap="vendor/autoload.php"colors="true"><testsuites><testsuitename="Unit"><directory>test/Unit</directory></testsuite></testsuites><coverage><include><directory>app</directory></include></coverage><php><envname="APP_ENV"value="testing"/><envname="DB_DRIVER"value="sqlite"/><envname="DB_DATABASE"value=":memory:"/><envname="REDIS_HOST"value="redis"/></php></phpunit>--- 第五步:分支策略与 Pipeline 触发规则 feature/* → 只跑 test(PHPStan + PHPUnit) main →test→ build → deploy:staging(自动)→ notify master →test→ build → deploy:production(手动确认)→ notify release/* →test→ build(镜像打 release tag) MR → 只跑 test,结果回写到 MR 页面 在 GitLab 项目 → Settings → CI/CD → General pipelines 中配置: - Default branch:main - Protected branches:master(只有 Maintainer 可以 push) - Merge request approvals:至少1人审批 --- 第六步:GitLab Container Registry 使用 GitLab CE 内置 Registry,无需额外搭建 Harbor:# 登录(CI 中用内置变量,本地开发用个人 Token)dockerlogin registry.your-company.com\-uyour-username\-pyour-personal-access-token# 拉取镜像dockerpull registry.your-company.com/your-group/hyperf-app:latest .env 中配置 Registry 地址:CI_REGISTRY=registry.your-company.comCI_REGISTRY_IMAGE=registry.your-company.com/your-group/hyperf-app --- 关键注意事项 ┌─────────────────────┬──────────────────────────────────────────────────────────────────┐ │ 问题 │ 解决方案 │ ├─────────────────────┼──────────────────────────────────────────────────────────────────┤ │ Runner 拉镜像慢 │ config.toml 中 pull_policy=["if-not-present"]+ 阿里云镜像加速 │ ├─────────────────────┼──────────────────────────────────────────────────────────────────┤ │ Composer 下载慢 │ 全局配置阿里云镜像,COMPOSER_MIRROR 变量注入 │ ├─────────────────────┼──────────────────────────────────────────────────────────────────┤ │ vendor/ 跨 Job 传递 │ artifacts 传递(同 Pipeline),cache 跨 Pipeline 复用 │ ├─────────────────────┼──────────────────────────────────────────────────────────────────┤ │ SSH 私钥换行问题 │ GitLab Variables 类型选 File,tr-d'\r'处理 Windows 换行 │ ├─────────────────────┼──────────────────────────────────────────────────────────────────┤ │ 生产零停机 │ 先启新容器健康检查通过后再切 Nginx upstream,再停旧容器 │ ├─────────────────────┼──────────────────────────────────────────────────────────────────┤ │ 镜像体积 │ 多阶段构建,生产镜像不含 composer、git、开发依赖 │ ├─────────────────────┼──────────────────────────────────────────────────────────────────┤ │ 私有包认证 │ COMPOSER_AUTH 变量设为 Masked,CI 中写入 auth.json │