hyperf对接项目接入 GitLab CI 国内部署
整体架构 开发者 push / MR ↓ esc to interrupt GitLab CE私有化 / 极狐 ↓ 触发 Pipeline GitLab RunnerDocker executor ↓ ┌──────────────────────────────────────┐ │ stage:test→ PHPStan PHPUnit │ │ stage: build → Docker 镜像构建 │ │ stage: push → 推送私有 Registry │ │ stage: deploy → SSH 平滑部署 │ │ stage: notify → 钉钉/企业微信通知 │ └──────────────────────────────────────┘ --- 第一步GitLab CE 私有化部署 国内推荐两个选择 - 极狐 GitLabJiHu官方中文版镜像在国内速度快 → registry.gitlab.cn - GitLab CE 官方用阿里云镜像加速拉取1.1docker-compose.yml极狐版推荐国内 version:3.8services: 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_urlhttps://gitlab.your-company.comgitlab_rails[time_zone]Asia/Shanghai# 内置 Container Registryregistry_external_urlhttps://registry.your-company.comgitlab_rails[registry_enabled]true# 邮件可选gitlab_rails[smtp_enable]truegitlab_rails[smtp_address]smtp.exmail.qq.comgitlab_rails[smtp_port]465gitlab_rails[smtp_user_name]ciyour-company.comgitlab_rails[smtp_password]your-smtp-passwordgitlab_rails[smtp_domain]your-company.comgitlab_rails[smtp_authentication]logingitlab_rails[smtp_enable_starttls_auto]truegitlab_rails[smtp_tls]true# 性能优化4C8G 机器puma[worker_processes]2sidekiq[concurrency]10postgresql[shared_buffers]256MBports: -80:80-443:443-22:22volumes: - /data/gitlab/config:/etc/gitlab - /data/gitlab/logs:/var/log/gitlab - /data/gitlab/data:/var/opt/gitlab shm_size:256mgitlab-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|grepgitlab 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注册 RunnerDocker executordockerexec-itgitlab-runner gitlab-runner register\--non-interactive\--urlhttps://gitlab.your-company.com\--tokenglrt-your-runner-token\--executordocker\--docker-imageregistry.cn-hangzhou.aliyuncs.com/hyperf/hyperf:8.1-alpine-v3.16-swoole\--docker-volumes/var/run/docker.sock:/var/run/docker.sock\--docker-privileged\--descriptionhyperf-runner\--tag-listphp,hyperf,docker\--run-untaggedtrue\--lockedfalse2.3优化 Runner 配置/data/gitlab-runner/config/config.toml concurrent4# 最多同时跑 4 个 Jobcheck_interval3[[runners]]namehyperf-runnerurlhttps://gitlab.your-company.comtokenglrt-your-runner-tokenexecutordocker[runners.docker]imageregistry.cn-hangzhou.aliyuncs.com/hyperf/hyperf:8.1-alpine-v3.16-swooleprivilegedtruevolumes[/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]TypelocalPath/cacheSharedtrue--- 第三步项目配置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_SHAIMAGE_LATEST:$CI_REGISTRY_IMAGE:latest# Composer 阿里云镜像COMPOSER_MIRROR:https://mirrors.aliyun.com/composer/# 关闭 Swoole 短名称Hyperf 必须PHP_INI_SCAN_DIR:# ── 全局 before_script ────────────────────────────────────default: before_script: -echoAsia/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~/.sshchmod700~/.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--level5--no-progress --error-formatgitlab app/phpstan-report.json||true- php vendor/bin/phpstan analyse--level5app/ artifacts: reports: codequality: phpstan-report.json expire_in:1week rules: - if:$CI_PIPELINE_SOURCE push- if:$CI_PIPELINE_SOURCE merge_request_eventphpunit: 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--colorsnever 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:/certsDOCKER_BUILDKIT:1needs:[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_CACHE1--build-argAPP_ENVproduction--labelgit.commit$CI_COMMIT_SHA--labelgit.branch$CI_COMMIT_REF_NAME--labelbuild.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-oStrictHostKeyCheckingno${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-oStrictHostKeyCheckingno${DEPLOY_USER}${DEPLOY_HOST} set -e echo 备份当前版本... BACKUP_TAG\$(dockerinspect hyperf-app--format{{.Config.Image}}2/dev/null||echonone)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 masterwhen: 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-appPHP_BIN:/usr/local/php/bin/php: *ssh-setup script: -|ssh-oStrictHostKeyCheckingno${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 masterwhen: 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-XPOSThttps://oapi.dingtalk.com/robot/send?access_token${DINGTALK_TOKEN}\-HContent-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 masterwhen: on_success notify:failure: stage: notify image: alpine:3.18 script: - apkadd--no-cachecurl-|curl-s-XPOSThttps://oapi.dingtalk.com/robot/send?access_token${DINGTALK_TOKEN}\-HContent-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 masterwhen: 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--fromdeps /app.# 运行时配置RUNechoswoole.use_shortnameOff/etc/php8/conf.d/swoole.ini\mkdir-pruntime/logs\chmod-R777runtime EXPOSE9501STOPSIGNAL SIGTERM HEALTHCHECK--interval10s--timeout5s--retries3\CMDcurl-sfhttp://127.0.0.1:9501/health||exit1CMD[php,/app/bin/hyperf.php,start]phpunit.xml?xmlversion1.0encodingUTF-8?phpunitbootstrapvendor/autoload.phpcolorstruetestsuitestestsuitenameUnitdirectorytest/Unit/directory/testsuite/testsuitescoverageincludedirectoryapp/directory/include/coveragephpenvnameAPP_ENVvaluetesting/envnameDB_DRIVERvaluesqlite/envnameDB_DATABASEvalue:memory:/envnameREDIS_HOSTvalueredis//php/phpunit--- 第五步分支策略与 Pipeline 触发规则 feature/* → 只跑 testPHPStan 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 branchmain - Protected branchesmaster只有 Maintainer 可以 push - Merge request approvals至少1人审批 --- 第六步GitLab Container Registry 使用 GitLab CE 内置 Registry无需额外搭建 Harbor# 登录CI 中用内置变量本地开发用个人 Tokendockerlogin registry.your-company.com\-uyour-username\-pyour-personal-access-token# 拉取镜像dockerpull registry.your-company.com/your-group/hyperf-app:latest .env 中配置 Registry 地址CI_REGISTRYregistry.your-company.comCI_REGISTRY_IMAGEregistry.your-company.com/your-group/hyperf-app --- 关键注意事项 ┌─────────────────────┬──────────────────────────────────────────────────────────────────┐ │ 问题 │ 解决方案 │ ├─────────────────────┼──────────────────────────────────────────────────────────────────┤ │ Runner 拉镜像慢 │ config.toml 中 pull_policy[if-not-present] 阿里云镜像加速 │ ├─────────────────────┼──────────────────────────────────────────────────────────────────┤ │ Composer 下载慢 │ 全局配置阿里云镜像COMPOSER_MIRROR 变量注入 │ ├─────────────────────┼──────────────────────────────────────────────────────────────────┤ │ vendor/ 跨 Job 传递 │ artifacts 传递同 Pipelinecache 跨 Pipeline 复用 │ ├─────────────────────┼──────────────────────────────────────────────────────────────────┤ │ SSH 私钥换行问题 │ GitLab Variables 类型选 Filetr-d\r处理 Windows 换行 │ ├─────────────────────┼──────────────────────────────────────────────────────────────────┤ │ 生产零停机 │ 先启新容器健康检查通过后再切 Nginx upstream再停旧容器 │ ├─────────────────────┼──────────────────────────────────────────────────────────────────┤ │ 镜像体积 │ 多阶段构建生产镜像不含 composer、git、开发依赖 │ ├─────────────────────┼──────────────────────────────────────────────────────────────────┤ │ 私有包认证 │ COMPOSER_AUTH 变量设为 MaskedCI 中写入 auth.json │