Spring Boot 项目 GitLab CI/CD 自动构建并推送到 Harbor 教程

创建一条稳定、可审计的流水线:提交即构建,分支/标签分别产出测试/生产镜像,缓存加速、凭据安全、日志清晰。下面给出一篇从零到一的完整教程,并提供可直接复制使用的 .gitlab-ci.yml


前提与准备

  • 环境组件:
    • GitLab: 14+(CE/EE 或 GitLab.com
    • GitLab Runner: Docker executor,Runner 主机已安装 Docker 并允许作业访问宿主机 Docker(通过 DOCKER_HOST=unix:///var/run/docker.sock
    • Harbor: 2.5+,已启用 HTTPS,创建了项目(如 ayy-server
  • 凭据与变量(GitLab 项目或组级 CI/CD Variables):
    • HARBOR_USER: Harbor 用户或 Robot 账户(推荐机器人账号)
    • HARBOR_PASS: 对应密码或 token(Masked、Protected)
    • SSH_PRIVATE_KEY: 若需要后续 SSH 部署(可选,Masked)
  • Runner 节点证书(如 Harbor 使用自签名证书):
    • Harbor CA: 放到 Runner 节点 /etc/docker/certs.d/192.168.0.12:5080/ca.crt,并重启 Docker
  • Dockerfile(推荐多阶段构建 + 非 root 用户):
    • 使用你已有的多阶段 Dockerfile(Maven 构建 + 精简 JRE 运行),并确保端口、配置目录、日志目录、环境变量设置合理

流水线设计与策略

  • 分支/环境策略:
    • main 分支: 产出测试镜像,标签为 test-${CI_COMMIT_SHORT_SHA}test-latest
    • tag 发布: 产出生产镜像,标签为 vX.Y.Zprod-latest(强制校验版本格式)
  • 构建缓存:
    • 使用 BuildKit 内联缓存 + "缓存镜像"标签(test-cache/prod-cache)提升二次构建速度
  • 登录与推送:
    • 每个作业在 before_script 里登录 Harbor,推送成功后清理本地镜像,保持 Runner 干净
  • 凭据安全:
    • Harbor 凭据使用变量注入,不写入仓库;变量设为 Masked + Protected
  • 可审计性:
    • 构建阶段生成 build.env(dotenv artifacts),在后续阶段复用镜像标签与环境信息

完整 .gitlab-ci.yml(可复制使用)

yaml 复制代码
stages:
  - build
  - push

variables:
  DOCKER_HOST: unix:///var/run/docker.sock
  DOCKER_BUILDKIT: "1"
  REGISTRY: 192.168.0.12:5080
  PROJECT: ayy-server
  IMAGE_NAME: ayy-server-java17
  GIT_LFS_SKIP_SMUDGE: "1"

# ==================== 公共锚点 ====================
.docker-base: &docker-base
  image: docker:20.10.24
  before_script:
    # 验证 Docker 可用(使用宿主机 Docker)
    - echo "使用宿主机 Docker"
    - docker info
    - docker version
    # 登录 Harbor
    - echo "$HARBOR_PASS" | docker login -u "$HARBOR_USER" --password-stdin $REGISTRY

.ssh-base: &ssh-base
  image: alpine:latest
  before_script:
    - apk add --no-cache openssh-client curl
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh

# Maven 缓存(加速依赖下载)
cache:
  key: maven-cache
  paths:
    - .m2/repository

# ==================== 测试环境(main 分支) ====================
build-test:
  <<: *docker-base
  stage: build
  script:
    - export IMAGE_TAG="test-${CI_COMMIT_SHORT_SHA}"
    - export CACHE_IMAGE="$REGISTRY/$PROJECT/$IMAGE_NAME:test-cache"
    - echo "开始构建测试镜像 $IMAGE_TAG"

    # 尝试拉取缓存镜像
    - docker pull $CACHE_IMAGE || echo "缓存镜像不存在,将从头构建"

    # 构建镜像
    - |
      docker build \
        --progress=plain \
        --cache-from $CACHE_IMAGE \
        --build-arg BUILDKIT_INLINE_CACHE=1 \
        --build-arg BUILD_TIME=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
        --build-arg GIT_COMMIT=$CI_COMMIT_SHORT_SHA \
        -t $REGISTRY/$PROJECT/$IMAGE_NAME:$IMAGE_TAG \
        -t $REGISTRY/$PROJECT/$IMAGE_NAME:test-latest \
        -t $CACHE_IMAGE \
        .

    - echo "✓ 镜像构建成功"
    - docker images | grep $IMAGE_NAME

    # 保存构建信息
    - echo "IMAGE_TAG=$IMAGE_TAG" > build.env
    - echo "ENVIRONMENT=test" >> build.env
  artifacts:
    reports:
      dotenv: build.env
    expire_in: 1 day
  only:
    - main
  tags:
    - jdk17
  retry:
    max: 2
    when:
      - runner_system_failure
      - stuck_or_timeout_failure

push-test:
  <<: *docker-base
  stage: push
  dependencies:
    - build-test
  script:
    - echo "开始推送测试镜像 $IMAGE_TAG"
    - docker push $REGISTRY/$PROJECT/$IMAGE_NAME:$IMAGE_TAG
    - docker push $REGISTRY/$PROJECT/$IMAGE_NAME:test-latest
    - docker push $REGISTRY/$PROJECT/$IMAGE_NAME:test-cache
    - echo "✓ 镜像推送成功"

    # 显示推送的镜像信息
    - echo "推送的镜像:"
    - echo "  - $REGISTRY/$PROJECT/$IMAGE_NAME:$IMAGE_TAG"
    - echo "  - $REGISTRY/$PROJECT/$IMAGE_NAME:test-latest"
  after_script:
    # 清理本地镜像
    - docker rmi $REGISTRY/$PROJECT/$IMAGE_NAME:$IMAGE_TAG || true
    - docker rmi $REGISTRY/$PROJECT/$IMAGE_NAME:test-latest || true
    - docker system prune -f || true
  only:
    - main
  tags:
    - jdk17
  retry:
    max: 2
    when:
      - runner_system_failure

# ==================== 生产环境(tag 发布) ====================
build-prod:
  <<: *docker-base
  stage: build
  script:
    - export IMAGE_TAG="${CI_COMMIT_TAG}"
    - export CACHE_IMAGE="$REGISTRY/$PROJECT/$IMAGE_NAME:prod-cache"
    - echo "开始构建生产镜像 $IMAGE_TAG"

    # 验证标签格式 (v1.0.0)
    - |
      if ! echo "$IMAGE_TAG" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
        echo "❌ 错误: 标签格式必须为 vX.Y.Z (例如: v1.0.0)"
        echo "当前标签: $IMAGE_TAG"
        exit 1
      fi
    - echo "✓ 标签格式验证通过 $IMAGE_TAG"

    # 尝试拉取缓存镜像
    - docker pull $CACHE_IMAGE || echo "缓存镜像不存在,将从头构建"

    # 构建镜像
    - |
      docker build \
        --progress=plain \
        --cache-from $CACHE_IMAGE \
        --build-arg BUILDKIT_INLINE_CACHE=1 \
        --build-arg BUILD_TIME=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
        --build-arg GIT_TAG=$CI_COMMIT_TAG \
        -t $REGISTRY/$PROJECT/$IMAGE_NAME:$IMAGE_TAG \
        -t $REGISTRY/$PROJECT/$IMAGE_NAME:prod-latest \
        -t $CACHE_IMAGE \
        .

    - echo "✓ 镜像构建成功"
    - docker images | grep $IMAGE_NAME

    # 保存构建信息
    - echo "IMAGE_TAG=$IMAGE_TAG" > build.env
    - echo "ENVIRONMENT=production" >> build.env
  artifacts:
    reports:
      dotenv: build.env
    expire_in: 30 days
  only:
    - tags
  tags:
    - jdk17
  retry:
    max: 2
    when:
      - runner_system_failure
      - stuck_or_timeout_failure

push-prod:
  <<: *docker-base
  stage: push
  dependencies:
    - build-prod
  script:
    - echo "开始推送生产镜像 $IMAGE_TAG"
    - docker push $REGISTRY/$PROJECT/$IMAGE_NAME:$IMAGE_TAG
    - docker push $REGISTRY/$PROJECT/$IMAGE_NAME:prod-latest
    - docker push $REGISTRY/$PROJECT/$IMAGE_NAME:prod-cache
    - echo "✓ 镜像推送成功"

    # 显示推送的镜像信息
    - echo "推送的镜像:"
    - echo "  - $REGISTRY/$PROJECT/$IMAGE_NAME:$IMAGE_TAG"
    - echo "  - $REGISTRY/$PROJECT/$IMAGE_NAME:prod-latest"
  after_script:
    # 清理本地镜像
    - docker rmi $REGISTRY/$PROJECT/$IMAGE_NAME:$IMAGE_TAG || true
    - docker rmi $REGISTRY/$PROJECT/$IMAGE_NAME:prod-latest || true
    - docker system prune -f || true
  only:
    - tags
  tags:
    - jdk17
  retry:
    max: 2
    when:
      - runner_system_failure

关键点讲解与常见坑

  • Docker 访问模式:
    • 说明: 该流水线采用"使用宿主机 Docker"的模式(非 DIND 服务),通过 DOCKER_HOST=unix:///var/run/docker.sock 直接访问 Runner 主机的 Docker。优点是不需要 privileged,性能和兼容性好;缺点是需确保 Runner 主机安全与权限隔离。
  • Harbor 登录与命名:
    • 仓库路径: $REGISTRY/$PROJECT/$IMAGE_NAME:<tag>(如 192.168.0.12:5080/ayy-server/ayy-server-java17:test-latest
    • 建议: 使用项目/应用分层命名;避免顶层 library;区分 test-*prod-* 标签。
  • 构建缓存:
    • 机制: --build-arg BUILDKIT_INLINE_CACHE=1 + --cache-from <cache image> 搭配指定的缓存镜像标签,提升二次构建速度。
    • 前提: 第一次构建没有缓存是正常的;后续才会生效。
  • 版本规范校验(生产):
    • 规则: vX.Y.Z(语义化版本);不符合直接失败,避免随意发布"生产"镜像。
  • 证书与私有仓库:
    • 自签名: Runner 主机需信任 Harbor CA;如果仍报 x509,确认端口与域名一致、证书链完整,必要时将证书也挂载到作业环境。
  • 清理策略:
    • after_script: 清理构建镜像与 dangling 层,防止 Runner 主机长期磁盘膨胀。

与 Dockerfile 的配合要点

  • 构建参数联动:
    • 建议: 在 Dockerfile(构建阶段)里接受 BUILD_TIME, GIT_COMMIT/GIT_TAG,打入镜像元信息;便于审计与追溯。
  • 端口与配置:
    • 暴露端口: 与应用实际端口一致(示例为 48087)。
    • 配置目录: /app/config 支持外部挂载;流水线构建阶段不需携带敏感配置,部署时再注入。
  • 非 root 与权限:
    • 用户: 使用非 root 用户运行;确保日志目录与配置目录权限正确(避免 logback 写入失败)。

下一步:部署联动与质量门禁(可选增强)

  • 质量门禁:
    • 漏洞扫描: 在 Harbor 开启 Trivy;推送后自动扫描,发现高危阻断发布。
    • SBOM: 构建后生成 SBOM(如 Syft),存档以提升可追溯性。
  • 部署联动:
    • Kubernetes: 推送成功后,触发 Helm/Argo CD 更新;镜像标签使用 commit_sha 实现幂等与回滚。
    • VM/主机: 使用 ssh-base 锚点执行远程拉取与滚动重启脚本(确保蓝绿或最小化停机)。
相关推荐
L.EscaRC3 小时前
面向 Spring Boot 的 JVM 深度解析
jvm·spring boot·后端
老华带你飞3 小时前
订票系统|车票管理系统|基于Java+vue的车票管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·订票系统
陈果然DeepVersion3 小时前
Java大厂面试真题:Spring Boot+Kafka+AI智能客服场景全流程解析(十一)
java·spring boot·微服务·ai·kafka·面试题·rag
whltaoin4 小时前
【浏览器CORS问题解决方案】SpringBoot+Vue3前后端全覆盖:浏览器跨域问题的多样化解决方案
vue.js·spring boot·浏览器跨域问题
佐杰4 小时前
持续集成与持续部署
ci/cd
Mos_x4 小时前
【Spring Boot】Spring Boot解决循环依赖
java·spring boot·spring
亚林瓜子5 小时前
AWS Elastic Beanstalk中安装tesseract5.3.4版本
spring boot·ocr·tesseract·aws·beanstalk·tess4j·eb
一念一花一世界6 小时前
Argo CD vs Tekton vs Arbess,CI/CD工具一文纵评
ci/cd·tekton·argo cd·arbess
檐下翻书1737 小时前
Spring Boot 深度剖析:从虚拟线程到声明式 HTTP 客户端,再到云原生最优解
spring boot·http·云原生