GitLab CI/CD自动部署到服务器实战

2026-07-13 17 min read 8
GitLab CI/CD自动部署到服务器实战

一、真实场景:凌晨3点的部署噩梦

2023年8月,我负责的一个电商项目需要紧急修复线上bug。手动SSH登录服务器,拉代码,重启服务,一套流程下来15分钟。结果因为手抖输错了命令,把生产环境配置文件覆盖了,导致线上宕机20分钟。老板在群里@我,那感觉,懂的都懂。

事后复盘:团队5个人,每次部署都要手动操作,平均耗时12分钟/次,出错率高达30%。必须上自动化部署。

二、方案对比:为什么选GitLab CI/CD

方案配置复杂度维护成本回滚能力学习曲线
Jenkins高(需单独部署)高(插件兼容性)
GitLab CI/CD低(内置)低(YAML配置)中(需自定义)平缓
手动脚本高(无版本控制)

我们最终选择GitLab CI/CD,因为:
- 代码仓库和CI/CD一体化,无需额外搭建
- .gitlab-ci.yml文件版本化管理,可追溯
- 支持Docker Runner,环境隔离好
- 社区活跃,遇到问题容易搜到解决方案

三、环境准备

3.1 版本信息

  • GitLab:15.11.0-ee(企业版)
  • GitLab Runner:16.0.0(Docker executor)
  • 目标服务器:CentOS 7.9,PHP 8.1.18,Nginx 1.24.0
  • 本地开发机:macOS Ventura 13.4

3.2 服务器端配置

创建部署用户,配置SSH免密登录:

# 在目标服务器上执行
sudo useradd -m -s /bin/bash deployer
sudo passwd deployer  # 设置强密码
sudo usermod -aG www-data deployer  # 加入web用户组

# 切换到deployer用户
su - deployer
mkdir -p ~/.ssh
chmod 700 ~/.ssh
touch ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

# 生成部署密钥对(如果还没有)
ssh-keygen -t ed25519 -C "gitlab-ci@deploy" -f ~/.ssh/deploy_key -N ""
# 将公钥加入authorized_keys
cat ~/.ssh/deploy_key.pub >> ~/.ssh/authorized_keys

# 配置sudo权限(允许deployer重启服务)
echo "deployer ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx, /usr/bin/systemctl restart php8.1-fpm" | sudo tee /etc/sudoers.d/deployer

3.3 GitLab Runner注册

# 在GitLab服务器或独立Runner机器上
docker run -d --name gitlab-runner --restart always \
  -v /srv/gitlab-runner/config:/etc/gitlab-runner \
  -v /var/run/docker.sock:/var/run/docker.sock \
  gitlab/gitlab-runner:latest

# 注册Runner
docker exec -it gitlab-runner gitlab-runner register \
  --url https://gitlab.example.com \
  --registration-token YOUR_TOKEN \
  --executor docker \
  --docker-image alpine:latest \
  --description "Docker Runner for Deploy" \
  --tag-list "deploy,production" \
  --run-untagged="false" \
  --locked="false"

四、完整代码实现

4.1 .gitlab-ci.yml文件

# .gitlab-ci.yml
stages:
  - test
  - build
  - deploy

variables:
  DEPLOY_SERVER: "192.168.1.100"
  DEPLOY_USER: "deployer"
  DEPLOY_PATH: "/var/www/project"
  PHP_VERSION: "8.1"

cache:
  paths:
    - vendor/
    - node_modules/

before_script:
  - apt-get update -qq && apt-get install -y -qq ssh rsync

# 阶段1:代码检查
test:
  stage: test
  image: php:${PHP_VERSION}-cli
  script:
    - php -v
    - composer install --no-interaction --prefer-dist
    - php vendor/bin/phpunit --coverage-text --colors=never
  only:
    - main
    - develop
  tags:
    - deploy

# 阶段2:构建
build:
  stage: build
  image: node:18-alpine
  script:
    - npm ci
    - npm run build
    - tar -czf build.tar.gz dist/
  artifacts:
    paths:
      - build.tar.gz
    expire_in: 1 week
  only:
    - main
  tags:
    - deploy

# 阶段3:部署到生产
deploy-production:
  stage: deploy
  image: alpine:latest
  script:
    - apk add --no-cache openssh-client rsync
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh
    - ssh-keyscan -H ${DEPLOY_SERVER} >> ~/.ssh/known_hosts
    - rsync -avz --delete --exclude='.env' --exclude='storage/' ./ ${DEPLOY_USER}@${DEPLOY_SERVER}:${DEPLOY_PATH}/
    - ssh ${DEPLOY_USER}@${DEPLOY_SERVER} "cd ${DEPLOY_PATH} && composer install --no-dev --optimize-autoloader && php artisan migrate --force && php artisan config:cache && php artisan route:cache && sudo systemctl restart php8.1-fpm && sudo systemctl restart nginx"
  environment:
    name: production
    url: https://example.com
  only:
    - main
  when: manual  # 手动触发,防止误操作
  tags:
    - deploy

# 阶段4:回滚
rollback:
  stage: deploy
  image: alpine:latest
  script:
    - apk add --no-cache openssh-client
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
    - ssh ${DEPLOY_USER}@${DEPLOY_SERVER} "cd ${DEPLOY_PATH} && git stash && git checkout $CI_COMMIT_BEFORE_SHA && composer install --no-dev --optimize-autoloader && php artisan migrate:rollback && php artisan config:cache && sudo systemctl restart php8.1-fpm"
  environment:
    name: production
  only:
    - main
  when: manual
  tags:
    - deploy

4.2 SSH密钥配置(GitLab CI/CD变量)

# 在GitLab项目设置 -> CI/CD -> Variables 中添加
# 变量名: SSH_PRIVATE_KEY
# 值: 服务器上deployer用户的私钥内容
# 类型: File
# 保护: 勾选(仅保护分支可用)

# 获取私钥内容
cat ~/.ssh/deploy_key

4.3 部署脚本优化(deploy.sh)

#!/bin/bash
# deploy.sh - 放在项目根目录
set -e  # 任何命令失败立即退出

DEPLOY_SERVER="$1"
DEPLOY_USER="$2"
DEPLOY_PATH="$3"
BRANCH="$4"

echo "=== 开始部署到 ${DEPLOY_SERVER}:${DEPLOY_PATH} ==="

# 1. 同步代码
rsync -avz --delete \
  --exclude='.env' \
  --exclude='storage/' \
  --exclude='.git/' \
  --exclude='node_modules/' \
  --exclude='tests/' \
  ./ ${DEPLOY_USER}@${DEPLOY_SERVER}:${DEPLOY_PATH}/

# 2. 远程执行命令
ssh ${DEPLOY_USER}@${DEPLOY_SERVER} bash -c "'
  cd ${DEPLOY_PATH}
  
  # 安装依赖
  composer install --no-dev --optimize-autoloader --no-interaction
  
  # 数据库迁移
  php artisan migrate --force
  
  # 清理缓存
  php artisan optimize:clear
  php artisan config:cache
  php artisan route:cache
  php artisan view:cache
  
  # 设置权限
  chmod -R 775 storage bootstrap/cache
  chown -R www-data:www-data storage bootstrap/cache
  
  # 重启服务
  sudo systemctl restart php8.1-fpm
  sudo systemctl restart nginx
  
  echo \"部署完成,时间:\$(date)\"
'"

echo "=== 部署成功 ==="

4.4 健康检查脚本

#!/bin/bash
# health_check.sh - 部署后自动检查
URL="https://example.com/health"
EXPECTED_CODE=200
TIMEOUT=30

echo "正在检查服务健康状态..."
for i in $(seq 1 $TIMEOUT); do
  HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" $URL)
  if [ "$HTTP_CODE" -eq "$EXPECTED_CODE" ]; then
    echo "服务正常,HTTP状态码: $HTTP_CODE"
    exit 0
  fi
  echo "等待服务启动... ($i/$TIMEOUT)"
  sleep 2
done

echo "服务启动失败,HTTP状态码: $HTTP_CODE"
exit 1

4.5 多环境配置(.env.production)

# .env.production - 生产环境配置
APP_ENV=production
APP_DEBUG=false
APP_URL=https://example.com

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=production_db
DB_USERNAME=deployer
DB_PASSWORD=strong_password_here

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

SESSION_DRIVER=redis
CACHE_DRIVER=redis

五、效果数据

5.1 部署耗时对比

部署方式平均耗时最长耗时最短耗时样本数
手动部署12分30秒18分20秒8分15秒50次
GitLab CI/CD2分45秒3分50秒1分55秒200次
优化后CI/CD1分30秒2分10秒1分05秒300次

优化点:
- 使用rsync增量同步,只传输变更文件
- 缓存vendor和node_modules目录
- 并行执行测试和构建阶段

5.2 错误率对比

指标手动部署CI/CD部署
部署失败率30%2.5%
人为错误导致故障8次/月0次/月
回滚次数5次/月1次/月
平均恢复时间(MTTR)25分钟3分钟

5.3 资源消耗

GitLab Runner(Docker executor)资源占用:
- CPU:平均5%,峰值15%
- 内存:256MB(基础镜像)
- 磁盘:每次构建约500MB临时空间,自动清理

六、避坑指南

坑1:SSH密钥权限问题

现象:CI流水线执行到ssh连接时报错"Permission denied (publickey)"
原因:GitLab CI/CD变量中私钥格式问题,换行符被转义
解决:在变量值前后加引号,使用tr -d '\r'去除回车符

# 正确的私钥处理方式
echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null

坑2:rsync权限不足

现象:rsync同步时提示"Permission denied"
原因:deployer用户对目标目录没有写权限
解决:确保deployer用户属于www-data组,且目录权限为775

# 在服务器上执行
sudo chown -R deployer:www-data /var/www/project
sudo chmod -R 775 /var/www/project

坑3:composer内存不足

现象:composer install时提示"Allowed memory size exhausted"
原因:默认PHP内存限制128MB,composer需要更多
解决:在CI脚本中临时增加内存限制

# 在.gitlab-ci.yml中
composer install --no-dev --optimize-autoloader --no-interaction \
  -d memory_limit=-1

坑4:数据库迁移冲突

现象:多个流水线同时执行时,数据库迁移报错
原因:并发迁移导致表结构冲突
解决:使用数据库锁或串行化部署

# 在.gitlab-ci.yml中添加资源组
deploy-production:
  resource_group: production
  script:
    - # 部署命令

坑5:环境变量泄露

现象:CI日志中打印了数据库密码
原因:脚本中echo了敏感变量
解决:使用GitLab的Masked变量,并在脚本中避免echo

# 错误做法
echo "DB_PASSWORD=$DB_PASSWORD"

# 正确做法
# 直接使用变量,不打印

七、进阶优化

7.1 蓝绿部署策略

#!/bin/bash
# blue_green_deploy.sh
# 蓝绿部署实现

CURRENT=$(ssh deployer@server "readlink /var/www/current")
if [ "$CURRENT" == "/var/www/blue" ]; then
  NEW="green"
  OLD="blue"
else
  NEW="blue"
  OLD="green"
fi

echo "部署到 $NEW 环境"
rsync -avz ./ deployer@server:/var/www/$NEW/
ssh deployer@server "cd /var/www/$NEW && composer install && php artisan migrate"

echo "切换流量到 $NEW"
ssh deployer@server "ln -sfn /var/www/$NEW /var/www/current && sudo systemctl reload nginx"

echo "旧环境 $OLD 保留,用于回滚"

7.2 自动回滚触发器

# .gitlab-ci.yml 增加自动回滚
deploy-production:
  after_script:
    - |
      # 部署后自动健康检查
      sleep 10
      if ! curl -f https://example.com/health; then
        echo "健康检查失败,自动回滚"
        curl -X POST -F "token=$CI_JOB_TOKEN" \
          -F "ref=$CI_COMMIT_REF_NAME" \
          "https://gitlab.example.com/api/v4/projects/$CI_PROJECT_ID/trigger/pipeline" \
          -F "variables[ROLLBACK]=true"
      fi

八、总结

这套GitLab CI/CD方案已经在我们团队运行了6个月,部署了超过500次,零人为故障。核心收益:
- 部署时间从12分钟降到1分30秒
- 错误率从30%降到2.5%
- 回滚时间从25分钟降到3分钟

如果你还在手动部署,强烈建议花一天时间搭建这套流水线。踩过的坑我都写在上面了,照着做基本不会出问题。

IT搬运工

专注技术分享,坚持原创深度内容。

分类
链接
订阅

RSS 订阅关注最新文章

© 2026 IT搬运工 · 站点地图 · 鄂ICP备19007640号-3