本教程是一份关于如何在单台Linux服务器上使用Jenkins、Docker Compose和Traefik构建可用于生产环境的CI/CD管道的完整指南。

您将学习如何通过具有自动续期功能的HTTPS在自定义域名上公开服务,以及如何实施一种智能部署策略——该策略能够检测到变化,并且只重新部署受影响的微服务。这样就可以避免不必要的全栈应用重新部署。我们还会探讨一些实际生产环境中会遇到的问题以及针对这些问题的具体解决方法。

目录

1. 您将构建什么

在本教程中,您将在与应用程序堆栈相同的Linux服务器上,在Docker环境中运行Jenkins实例。

Traefik将作为Jenkins的前端反向代理服务器,通过一个简洁的URL地址(https://jenkins.example.com)来公开Jenkins服务,并且会使用自动续期的Let’s Encrypt证书进行安全保护。

您还需要在应用程序代码仓库中创建一个Jenkinsfile,该文件将会:

  • 在每次向staging分支推送代码时自动触发执行;

  • 检测每次提交中哪些微服务发生了变化;

    从主机服务器上拉取最新的代码;

    仅重新构建并重启受影响的服务

每次进行代码推送时,只有相关的服务才会被重新部署。

先决条件

在开始学习之前,本指南假定您已经熟悉一些核心概念和工具。

这不是针对初学者的教程——我们将直接操作基础设施、容器以及CI/CD管道。

您应该了解以下内容:

  • 基本的Linux命令(SSH、文件系统导航、权限设置)

  • Docker的基础知识(镜像、容器、卷、网络)

  • Git的工作流程(克隆、拉取、分支操作)

  • CI/CD管道的基本概念

所需的工具和环境:

  • 一台Linux服务器(推荐使用Ubuntu)

  • Docker引擎 + Docker Compose(v2版本)

  • 一个域名(用于Traefik和HTTPS服务)

  • 一个GitHub仓库(用于存放后端项目代码)

  • 对微服务架构有基本的了解

如果您已经掌握了上述内容,那么就可以开始学习了。

2. 架构

以下是该架构的概览:

┌──────────────────────────── Linux服务器(Ubuntu) ────────────────────────────┐
│                                                                               │
│   /home/developer/projects/                                                  │
│       └── project-prod-configs/             ← 基础设施配置仓库(包含Docker Compose和Traefik配置文件) │
│              ├── docker-compose.staging.yml                                   │
│              ├── traefik.staging.yml                                          │
│              └── project-backend/          ← 应用程序代码仓库(包含服务组件和网关配置) │
│                     ├── Jenkinsfile                                           │
│                     ├── docker-compose.staging.yml                            │
│                     └── apps/                                                 │
│                            ├── services//                               │
│                            ├── gateways//                               │
│                            └── core//                                   │
│                                                                               │
│   ┌─────────────────────── Docker网络:代理层 ──────────────────────┐      │
│   │  traefik (80, 443)                                                 │      │
│   │     │                                                              │      │
│   │     ├──► jenkins  (projects-jenkins-staging)                     │      │
│   │     │      ↳ /projects  ← 主机项目目录的挂载路径         │      │
│   │     │      ↳ /var/run/docker.sock ← 用于与主机Docker服务器通信       │      │
│   │     │                                                              │      │
│   │     └──► 您构建的服务组件及网关          │      │
│   └────────────────────────────────────────────────────────────────────┘      │
│                                                                               │
└───────────────────────────────────────────────────────────────────────┘
            ▲
            │ 接收到推送请求时触发Webhook回调            │
           GitHub: /project-backend (分支:staging)

这里有两个关键点:

  1. Jenkins运行在容器中,但它通过挂载`/var/run/docker.sock`来控制宿主机器上的Docker。同时,它还会将项目文件夹挂载为`/projects/…`,这样就可以进入宿主机上的实际代码目录,并在那里运行`docker compose`命令。

  2. `Jenkinsfile`位于应用程序仓库中,因此管道配置会与代码一起进行版本控制。Jenkins只需指向这个文件即可。

3. 服务器要求

在开始配置Jenkins或Traefik之前,我们需要先做好服务器的相关准备。

在这一步中,我们将完成以下操作:

  • 创建一个专门用于管理项目的Linux用户

  • 安装Docker和Docker Compose

  • 设置仓库的文件夹结构

这样就能确保我们的CI/CD管道在干净且可预测的环境中运行。


# 创建用于管理项目的Linux用户
sudo adduser developer

# 安装Docker和Docker Compose
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker developer

# 检查Docker Compose版本
docker compose version
# 输出格式为:Docker Compose v2.x.y

# 查找Docker Compose插件的安装路径
ls /usr/libexec/docker/cli-plugins/docker-compose
# (某些发行版可能使用路径/usr/lib/docker/cli-plugins/docker-compose)

# 设置项目文件夹结构
sudo mkdir -p /home/developer/project
sudo chown -R developer:developer /home/developer/project

# 克隆两个仓库到相应位置
cd /home/developer/projects
git clone https://github.com//projects-prod-configs.git
cd projects-prod-configs
git clone -b staging https://github.com//projects-backend.git

现在,你的目录结构应该如下:

/home/developer/projects/projects-prod-configs/projects-backend

请记住这个路径,因为`Jenkinsfile`会引用它。

DNS配置

在执行后续步骤之前,请将Jenkins子域的A记录指向服务器的公共IP地址,这样Let’s Encrypt才能通过HTTP验证来完成证书颁发流程:

jenkins.example.com   A   

4. Traefik——反向代理服务器

Traefik充当了你整个系统的入口点。它不会手动为每个服务配置端口,而是会自动完成以下操作:

  • 根据域名来路由流量

  • 使用Let’s Encrypt生成并更新HTTPS证书

  • 与Docker连接并动态检测各种服务

  • 简单来说,Traefik让你能够通过以下地址访问各种服务:

    https://jenkins.example.com
    https://api.example.com

    <…无需手动配置 NGINX 或管理 SSL 证书即可实现。

    在这种配置中,Traefik会监视Docker容器,并根据我们稍后定义的标签来路由流量。

    Traefik会为每个容器分配一个真实的域名和一张真实的证书,而且不需要为每个服务单独配置设置——你只需要添加一些标签即可。

    traefik.staging.yml(静态配置文件)

    请将此文件放在你的基础设施代码仓库的根目录下:

    api:
      dashboard: true
    
    entryPoints:
      web:
        address: ":80"
      websecure:
        address: ":443"
    
    certificatesResolvers:
      letsencrypt:
        acme:
          httpChallenge:
            entryPoint: web
          email: admin@example.com           # ← 请根据实际情况修改此地址
          storage: /etc/traefik/acme.json
    
    providers:
      docker:
        endpoint: "unix:///var/run/docker.sock"
        exposedByDefault: false              # 只有那些将`traefik.enable`设置为`true`的容器才会使用此配置
        network: proxy
      file:
        directory: /etc/traefik/dynamic
        watch: true
    
    log:
      level: INFO
    
    accessLog: {}
    

    docker-compose.staging.yml中定义的Traefik服务

    networks:
      proxy:
        name: proxy
        driver: bridge
      internal:
        name: internal
        driver: bridge
    
    volumes:
      acme-data:
      traefik-logs:
      jenkins-data:
    
    services:
      traefik:
        image: traefik:v2.11
        container_name: projects-traefik-staging
        restart: unless-stopped
        ports:
          - "80:80"        # HTTP端口(会自动重定向到HTTPS)
          - "443:443"      # HTTPS端口
          - "8080:8080"    # 用于访问Traefik控制面板(仅限内部使用——需通过防火墙进行保护)
        volumes:
          - /var/run/docker.sock:/var/run/docker.sock:ro
          - ./traefik.staging.yml:/etc/traefik/traefik.yml:ro
          - ./dynamic:/etc/traefik/dynamic:ro
          - acme-data:/etc/traefik           # 用于保存Let's Encrypt证书文件
          - traefik-logs:/var/log/traefik
        networks:
          - proxy
        command:
          - '--api.insecure=false'
          - '--api/dashboard=true'
          - '--providers.docker=true'
          - '--providers.docker.exposedbydefault=false'
          - '--providers.docker.network=proxy'
          - '--entrypoints.web.address=:80'
          - '--entrypoints.websecure.address=:443'
          - '--entrypoints.web.http.redirections.entryPoint.to=websecure'
          - '--entrypoints.web.http.redirections.entryPointscheme=https'
          - '--certificatesresolvers.letsencrypt.acme.httpchallenge=true'
          - '--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web'
          - '--certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL:-admin@example.com}'
          - '--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme.json'
          - '--log.level=INFO'
          - '--accesslog=true'
        labels:
          - "traefik.enable=true"
          - "traefik.docker.network=proxy"
          # 用于配置Traefik的控制面板
          - "traefik.http.routers.traefik-dash.rule=Host(`traefik.example.com`)"
          - "traefik.http.routers.traefik-dash.entrypoints=websecure"
          - "traefik.http.routers.traefik-dash.tls_certResolver=letsencrypt"
          - "traefik.http.routers.traefik-dash.service=api@internal"
    

    操作步骤如下:

    cd /home/developer/projects/projects-prod-configs
    docker compose -f docker-compose.staging.yml up -d traefik
    

    首次运行时,请关注日志——一旦DNS解析完成,Traefik会立即为控制台主机请求证书。

    docker logs -f projects-traefik-staging
    

    提示:在测试阶段,将ACME认证的配置端点切换为staging版本(acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory),这样即使DNS配置出错,也不会导致Let’s Encrypt的速率限制被触发。正式上线之前,请取消这一设置。

    5. 在Docker中运行Jenkins

    将这个Jenkins服务添加到相同的docker-compose.staging.yml文件中。其中的每一行代码都非常重要(注释部分会解释每行的作用)。

      jenkins:
        image: jenkins/jenkins:lts
        container_name: projects-jenkins-staging
        restart: unless-stopped
        user: root                           # 使用root用户可以避免处理UID/GID相关的问题
        environment:
          - JAVA_OPTS=-Xmx1g -Xms512m -Duser.timezone=Asia/Dhaka
          - TZ=Asia/Dhaka                    # 容器内的时区设置
          - JENKINS_OPTS=--prefix=/
        ports:
          - "3095:8080"                      # Web界面端口
          - "50000:50000"                    # 代理服务器端口
        volumes:
          - jenkins-data:/var/jenkins_home   # 存储Jenkins配置文件和项目数据
          - /var/run/docker.sock:/var/run/docker.sock                          # 允许容器与主机之间的通信
          - /usr/bin/docker:/usr/bin/docker                                     # 主机上的docker命令行工具
          - /usr/libexec/docker/cli-plugins:/usr/libexec/docker/cli-plugins:ro  # docker compose插件
          - /home/developer/projects:/projects                                # 项目文件目录
          - /etc/localtime:/etc/localtime:ro                                    # 使容器内的时间与主机保持一致
          - /etc/timezone:/etc/timezone:ro
        networks:
          - proxy
          - internal
        healthcheck:
          test: ['CMD', 'curl', '-f', 'http://localhost:8080/login']
          interval: 30s
          timeout: 10s
          retries: 5
          start_period: 120s
        deploy:
          resources:
            limits:
              memory: 1024M
    

    为什么需要设置user: root这是因为这样最简单地就可以共享docker.sock文件,并实现项目目录的绑定挂载,而无需处理UID/GID相关的问题。如果你更倾向于使用非root用户,那么就需要设置group: docker,并确保容器内文件夹的权限与主机上的权限一致——虽然这也是可行的,但超出了本文的范围。

    6. 通过Traefik将Jenkins暴露在域名下

    很多指南都会忽略这一部分内容。我们会在Jenkins服务上添加标签,这样Traefik就能自动识别并配置该服务了,因此完全不需要修改Traefik的配置文件。

      jenkins:
        # ... 上述所有内容 ...
        labels:
          - "traefik.enable=true"
          - "traefik.docker.network=proxy"
    
          # 1) 路由器 —— 匹配传入的请求主机
          - "traefik.http.routers.jenkins.rule=Host(`jenkins.example.com`)"
          - "traefik.http.routers.jenkins.entrypoints=websecure"
          - "traefik.http.routers.jenkins.tls_certResolver=letsencrypt"
          - "traefik.http.routers.jenkins.service=jenkins"
    
          # 2) 服务 —— 告诉 Traefik 哪个容器端口对应应用程序
          - "traefik.http.services.jenkins.loadbalancer.server.port=8080"
    
          # 3) 中间件 —— Jenkins 需要 X-Forwarded-Proto 这个头信息,以便确定请求是在 HTTPS 环境中传输的
          - "traefik.http.middlewares.jenkins-headers.headers.customrequestheaders.X-Forwarded-Proto=https"
          - "traefik.http.routers.jenkins.middlewares=jenkins-headers"
    

    每条命令的作用:

    标签 作用
    traefik.enable=true 启用此容器的Traefik功能(我们之前将exposedByDefault=false设置为禁用状态)。
    traefik.docker.network=proxy 指定Traefik应与Jenkins使用哪个网络进行通信(Jenkins同时连接在proxyinternal两个网络上)。
    routers.jenkins.rule=Host(...) 仅将指定的主机名转发到Jenkins。
    routers.jenkins.entrypoints=websecure 仅允许443端口的请求进入Jenkins(HTTP重定向配置已在第4节中说明)。
    routers.jenkins.tls.certresolver=letsencrypt 自动生成并更新SSL证书。
    services.jenkins.loadbalancer.server.port=8080 Jenkins在容器内部使用8080端口接收请求。
    customrequestheaders.X-Forwarded-Proto=https 如果不设置此项,Jenkins在生成Webhook链接时可能会使用错误的协议地址。

    启动Jenkins:

    cd /home/developer/projects/projects-prod-configs
    docker compose -f docker-compose.staging.yml up -d jenkins
    
    # 监控Traefik证书生成过程
    docker logs -f projects-traefik-staging | grep -i acme
    

    大约10到60秒后,你应该能够访问https://jenkins.example.com,并看到Jenkins的设置向导界面以及有效的锁图标。

    在首次登录Jenkins之后:

    进入“管理Jenkins” → “系统” → “Jenkins URL”,将其设置为:https://jenkins.example.com/

    这一点非常重要,因为Jenkins会使用这个基础URL来生成以下内容:

    • Webhook接口地址(用于GitHub触发器)

    • 邮件及构建日志中的链接地址

    如果设置不正确,GitHub的Webhook功能可能会失效,而且Jenkins生成的链接也会指向错误的地址(通常是localhost或内部IP地址)。

    7. 首次配置Jenkins

    如果你是第一次在這台服务器上运行Jenkins,请按照以下步骤完成初始设置。

    如果你已经配置好了Jenkins,可以跳过这一部分——但请确保所需的插件和设置与我们后续指南中的要求一致。

    1. 访问https://jenkins.example.com,获取初始管理员密码:

      docker exec projects-jenkins-staging cat /var/jenkins_home/secrets/initialAdminPassword
      
    2. 将密码粘贴进去,然后选择“安装推荐的插件”。

    3. 创建你的管理员用户。

    4. 进入“管理Jenkins” → “插件” → “可用的插件”,并安装以下插件:

      • GitHub(以及GitHub分支源代码插件)

      • Pipeline: GitHub插件

      • Credentials Binding插件(通常已预装)

    这就是您在本指南后续部分中所需的所有插件。

    8. 添加 GitHub 认证信息

    Jenkins 需要获得访问您的 GitHub 仓库的权限。

    这一权限是通过 GitHub 的个人访问令牌来实现的,该令牌相当于用于安全进行 API 操作和 Git 操作的密码。

    我们会在 Jenkins 中将这个令牌保存为认证信息,这样在执行管道任务时,Jenkins 就可以安全地拉取代码,而不会泄露任何敏感信息。

    这个唯一的认证信息既可用于从 SCM仓库中获取代码,也可用于部署时的 `git pull` 操作。

    1. 在 GitHub 上创建一个个人访问令牌,选择 repo 权限范围。

    2. 在 Jenkins 中:点击“管理 Jenkins” → “认证信息” → “系统” → “全局” → “添加认证信息”。

    3. 填写以下信息:

      • 类型:用户名+密码

      • 用户名:您的 GitHub 用户名

      • 密码:刚才创建的令牌

      • ID: github_classic_token Jenkinsfile 中会引用这个具体的 ID

    9. 创建管道任务

    现在 Jenkins 已经可以访问您的仓库了,下一步就是定义部署流程的具体执行方式。

    一个管道任务会告诉 Jenkins:

    • 代码存储的位置;

    • 需要监控哪个分支;

    • 以及如何执行部署过程。

    • 在 Jenkins 中,创建一个新的管道任务,并将其与您的 GitHub 仓库关联起来。一旦设置完成,每当您向 staging 分支推送代码时,Jenkins 就会自动触发部署流程。

      首先创建一个新任务:

      新建项目 → 管道任务 → 为其命名 projects-staging → 确认保存

      然后配置该任务:

      • 在“构建触发器”设置中,启用:
        用于 GitScm 监控的 GitHub 钩子触发器

      • 在“管道任务”设置中:

        • 定义方式:使用 SCM 中的管道脚本

        • SCM 工具:Git

        • 仓库地址:https://github.com/<org>/projects-backend.git

        • 认证信息:github_classic_token

        • 监控分支:*/staging

        • 脚本路径:Jenkinsfile

      保存配置设置。

      此时,Jenkins 已经完全连接到了您的仓库,并且已经准备好自动执行部署流程了。

      10. Jenkinsfile 文件(仅部署发生变更的部分)

      将这个文件放在 app 仓库的根目录下(路径为 projects-backend/Jenkinsfile),并确保它位于 staging 分支中。

      pipeline {
        agent any
      
        environment {
          PROJECT_PATH = "/projects/projects-prod-configs/projects-backend"
          COMPOSE_FILE = "docker-compose.staging.yml"
        }
      
        stages {
      
          stage('Checkout') {
            steps {
              checkout scm
              echo "分支 ${env.BRANCH_NAME ?: 'staging'}的检出操作已完成"
            }
          }
      
          stage('Detect Changes') {
            steps {
              script {
                def changedFiles = sh(
                  script: "git diff --name-only HEAD~1 HEAD",
                  returnStdout: true
                ).trim()
      
                echo "发生变更的文件列表:\n${changedFiles}"
      
                def services = [] as Set
                changedFiles.split('\n').each { file -> {
                  def svc = file =~ /^apps\/services\/([a-z0-9-]+)\//
                  def gw = file =~ /^apps\/gateways\/([a-z0-9-]+)\//
                  def core = file =~ /^apps\/core\/([a-z0-9-]+)\//
                  if (svc) { services << svc[0][1] }
                  if (gw) { services << gw[0][1] }
                  if (core) { services << core[0][1] }
                }
                services = services.findAll { !it.endsWith('-e2e') }
                env.CHANGED_SERVICES = services.join(' ')
      
                echo "需要部署的服务有: ${env.CHANGED_SERVICES ?: '(无)'}"
              }
            }
          }
      
          stage('Deploy') {
            when { expression { return env.CHANGED_SERVICES?.trim() } }
            steps {
              withCredentials([usernamePassword(
                credentialsId: 'github_classic_token',
                usernameVariable: 'GIT_USER',
                passwordVariable: 'GIT_TOKEN'
              )]) {
                sh '''
                  set -eu
                  git config --global --add safe_directory "${PROJECT_PATH}"
                  cd "${PROJECT_PATH}"
                  git remote set-url origin "https://github.com/<org>/projects-backend.git"
                  git -c credential.helper= \
                      -c "credential-helper=!f() { echo username=\({GIT_USER}; echo password=\){GIT_TOKEN}; }; f" \
                      pull origin staging
                  docker compose -f "\({COMPOSE_FILE}" up -d --build \){CHANGED_SERVICES}
                '''
              }
              echo "部署完成: ${env.CHANGED_SERVICES}"
            }
          }
      
          stage('Skip Deployment') {
            when { expression { return !env.CHANGED_SERVICES?.trim() } }
            steps { echo "未检测到任何服务变更——因此无需进行部署。" }
          }
        }
      }
      

      为何要设置这些复杂的步骤:

      • git config --global --add safe_directory ... — 这条命令用于确保Git不会对所有者UID与当前用户不同的仓库进行操作。磁盘上的仓库实际上是由developer账户拥有的,但容器内的Git进程是以root身份运行的,因此需要通过这条命令将某个路径添加到允许操作的目录列表中。

      • git remote set-url origin "https://..." — 这条命令会将磁盘上的远程仓库地址改为HTTPS协议,这样就可以使用token进行身份验证了。(普通的PAT机制无法用于验证git@github.com:这类URL,因为这些地址是通过SSH协议进行连接的。)这条命令是可重复执行的,因此可以放心多次运行。

      • git -c credential.helper="!f() { echo username=...; echo password=...; }; f" — 这条命令会在执行特定命令时将用户名和token传递给Git,而不会将这些信息保存到磁盘上,也不会在进程命令行中显示出来。

      • ${CHANGED_SERVICES} 这个变量没有加上引号,这样多个服务名称就可以被当作单独的参数来处理了。

      11. 端到端测试

      在确认所有设置都完成后,我们还需要验证整个流程是否能够按照预期正常运行。

      这次端到端测试可以确保:

      • GitHub的webhook能够正确触发Jenkins;

      • Jenkins能够识别出哪些服务发生了变化;

      • 并且只有发生变化的服务才会被重新构建并部署。

      • 换句话说,这个测试模拟了实际的生产环境中的部署过程。

        首先在你的仓库中做出一个小的修改,比如修改apps/gateways/student-apigw/目录下的某个文件,然后把这个更改推送到staging分支上。

        推送成功后,Jenkins应该会通过webhook自动触发构建过程。如果没有自动触发,你可以手动点击立即构建按钮。

        接下来打开构建过程的控制台输出,查看整个执行流程。你应该会看到类似以下的输出内容:

        • 已成功检出staging分支

        • 需要部署的服务:student-apigw

        • git pull origin staging (操作成功)

        • docker compose ... up -d --build student-apigw

        • 服务已成功部署:student-apigw

        如果看到了这样的输出顺序,那就说明你的管道配置是正确的。

        如果遇到任何问题,请不要担心——可以参考第12节,其中详细记录了各种常见问题及其解决方法。

        12. 故障排除——解决我们遇到的所有问题

        这一节会介绍在设置这个管道过程中遇到的实际问题,更重要的是,会解释为什么每种解决方法都能奏效。理解这些原因有助于你在自己遇到类似问题时进行调试。

        cd: 无法进入 /projects/projects-prod-configs/projects-backend 目录

        原因:
        Jenkinsfile中包含了cd $PROJECT_PATH这条命令,但在容器内部,这个路径并不存在。这种情况通常发生在以下情况下:…

        • 该项目没有在主机上被克隆出来,或者

        • 绑定挂载的配置有误。

        解决方法:

        ls /home/developer/projects/projects-prod-configs/projects-backend
        # 如果缺少该目录,请执行:git clone -b staging 
        

        确认绑定挂载是否成功:

        docker inspect projects-jenkins-staging --format "{{range .Mounts}}{{.Source}} -> {{.Destination}}{{println}}{{end}}'
        

        如果绑定挂载失败,请重新创建容器:

        docker compose -f docker-compose.staging.yml up -d --force-recreate jenkins
        

        为什么这种方法有效:

        Jenkins运行在容器内部,而你的代码文件位于主机上。通过绑定挂载,可以使得Jenkins能够访问主机上的项目目录。

        fatal: 检测到仓库所有权存在疑虑

        原因:
        当仓库的所有者与当前使用用户不同时,Git会阻止访问该仓库。

        • 仓库所有者:developer(主机端用户)

        • Git在容器内部以root账户运行

        解决方法:

        git config --global --add safe_directory "${PROJECT_PATH}"
        

        为什么这种方法有效:

        这个命令明确告诉Git,该目录是可信任的,从而绕过了因所有权不匹配而导致的访问限制。

        主机密钥验证失败 / 无法从远程仓库读取数据

        原因:

        该仓库使用SSH进行连接(格式为git@github.com:...),但是:

        • 容器内部没有SSH密钥

        • 系统中也不存在known_hosts文件

        此外,GitHub提供的token也无法通过SSH进行身份验证。

        推荐解决方法:

        git remote set-url origin "https://github.com/<org>>/projects-backend.git"
        

        为什么这种方法有效:

        HTTPS使用基于token的身份验证机制,因此即使在容器内部,也不需要配置SSH即可正常使用。

        docker compose命令中出现了未知的简写参数“f”

        原因:
        Docker CLI确实存在,但容器内部缺少Docker Compose插件。

        解决方法:

        volumes:
          - /usr/libexec/docker/cli-plugins:/usr/libexec/docker/cli-plugins:ro
        

        如果需要查找该插件的具体路径,可以执行以下命令:

        find /usr -name docker-compose -type f 2>&/dev/null
        

        验证插件是否已经安装成功:

        docker exec projects-jenkins-staging docker compose version
        

        为什么这种方法有效:

        Docker Compose v2实际上是一个CLI插件。通过将这个插件目录挂载到容器中,就可以在容器内部使用docker compose命令了。

        构建时间戳及Jenkins用户界面中显示的时区错误

        解决方法:需要同时设置环境变量和JVM参数,并将主机系统的时钟文件挂载到容器中:

        environment:
          - TZ=Asia/Dhaka
          - JAVA_OPTS=... -Duser.timezone=Asia/Dhaka
        volumes:
          - /etc/localtime:/etc/localtime:ro
          - /etc/timezone:/etc/timezone:ro
        

        为了使环境变量的更改生效,必须重新创建容器:

        docker compose -f docker-compose.staging.yml up -d --force-recreate jenkins
        

        原因说明:Jenkins是基于Java运行的,而Java使用的是与操作系统不同的时区设置。通过将操作系统的时区、JVM的时区以及主机系统的时钟设置为相同值,就可以确保所有地方的时间戳都是一致的。

        使用pnpm进行安装时会出现ERR SOCKET_TIMEOUT错误

        如果同时有多个服务在运行,并且每个服务都会使用大约1500个包来进行安装操作,那么网络负载会变得非常重,从而导致超时现象的发生。

        a) 增加超时时间并控制并发数量

        RUN pnpm install --frozen-lockfile --ignore-scripts 
        --network-timeout 600000 
        --network-concurrency 8
        

        这样可以为pnpm提供更多的执行时间,从而减少网络负载。

        b>启用pnpm缓存功能(适用于BuildKit环境)

        RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store 
        pnpm install --frozen-lockfile --ignore-scripts
        

        这样就可以将依赖项缓存起来,避免每次都需要重新下载这些依赖项。

        c>避免不必要的重新构建操作

        docker compose -f \(COMPOSE_FILE build \)CHANGED_SERVICES docker compose -f \(COMPOSE_FILE up -d --no-build \)CHANGED_SERVICES
        

        这样只有发生了变化的服务才会被重新构建,从而减少网络负载并降低失败率。

        编辑docker-compose.yml文件后,容器配置的更改并不会得到应用

        使用`docker compose up -d`命令时,系统并不会更新正在运行的容器。

        docker compose -f docker-compose.staging.yml up -d --force-recreate jenkins
        

        原因说明:这种命令会强制Docker重新创建容器,并应用更新后的配置参数(包括环境变量、挂载的文件等)。

        DNS设置不正确,导致服务器的80端口无法被访问;或者Docker网络配置有误。

        dig +short jenkins.example.com docker logs projects-traefik-staging 2>&&1 | grep -i acme

        原因说明:通过这些命令可以检查DNS配置是否正确,以及服务器的日志信息中是否有关于HTTPS连接的错误提示。

        Let’s Encrypt使用HTTP-01挑战机制,因此它必须通过80端口到达你的服务器。如果DNS配置或网络连接有问题,证书发放就会失败。

        Jenkins:“反向代理设置出现故障”

        解决方法:

        将Jenkins的URL设置为https://jenkins.example.com/
        确保添加以下头部信息:

        X-Forwarded-Proto: https

        为什么这样设置有效:

        Jenkins需要知道自己运行在HTTPS环境下。如果没有这个头部信息,它就会生成错误的URL(使用http而非https),从而导致重定向和Webhook功能无法正常工作。

        13. 主机与容器的概念区分

        许多配置错误都是由于将主机文件系统与容器文件系统混淆所导致的。下表可以清楚地说明这两者之间的区别:

        Jenkins容器内部 来自主机的内容
        /var/jenkins_home docker卷jenkins-data(包含Jenkins配置、作业信息及密钥等)
        /projects/... /home/developer/projects/...(你的项目目录结构)
        /usr/bin/docker 主机的/usr/bin/docker
        /usr/libexec/docker/cli-plugins/docker-compose 主机上的插件(用于支持docker compose命令)
        /var/run/docker.sock 主机的Docker守护进程(因此构建操作是在主机上进行的)
        /etc/localtime, /etc/timezone 主机的时钟设置
        ~/.ssh 无对应内容——因此如果没有额外配置,通过SSH连接到GitHub是无法正常工作的

        在调试时,一定要问自己:"这条命令是在哪个文件系统环境中运行的?它要查找的文件或文件夹在那里存在吗?"

        14. 日常操作快速指南

        # 在修改docker-compose配置后重新创建Jenkins容器
        cd /home/developer/Projects/projects-prod-configs
        docker compose -f docker-compose.staging.yml up -d --force-recreate jenkins
        
        # 查看Jenkins日志
        docker logs -f projects-jenkins-staging
        
        # 进入Jenkins容器内部执行命令
        docker exec -it projects-jenkins-staging bash
        
        # 在容器内部进行基本检查
        docker compose version
        ls /projects/projects-prod-configs/projects-backend
        git -C /projects/projects-prod-configs/projects-backend remote -v
        
        # 手动触发与管道相同的部署流程
        cd /projects/projects-configs/projects-backend
        git pull origin staging
        docker compose -f docker-compose.staging.yml up -d --build student-apigw
        
        # 检查Traefik的路由配置
        docker logs projects-traefik-staging 2>&1 | grep -i jenkins
        
        # 查看更新的证书信息
        docker exec projects-traefik-staging cat /etc/traefik/acme.json | head -50

        15. 下次我会采取哪些不同的措施

        • 预先构建一个基础镜像,并将所有的 node_modules 都包含在其中。由于有大约 1500 个包和 15 个服务,每次进行清洁构建时都会下载约 22,000 个 tar 文件。如果使用共享的基础镜像,这个数量可以减少 90%。

        • 在相同的 Docker 网络中运行私有的 npm 代理服务器(如 Verdaccio、Nexus 或 GitHub Packages),这样就可以完全避免 npmjs.org 带来的延迟问题。

        • 如果不同的服务在使用的工具上存在差异,应该为每个服务单独创建 Jenkinsfile。这样,所有团队都可以使用相同的构建流程定义。

        • git diff HEAD~1 HEAD 替换为 git diff $(git merge-base HEAD origin/staging~1) HEAD,这样在执行合并操作时就不会意外地忽略某些服务。

        • 应该将敏感信息存储在安全的密码管理系统中(如 HashiCorp Vault、AWS Secrets Manager 或 Doppler)。虽然 Jenkins 中的 PATs 也可以用来保护这些信息,但在多个任务中同时使用它们会带来很多麻烦。

        • 利用 Jenkins 的“配置即代码”功能(JCasC),将整个 Jenkins 构置环境(包括作业、凭证设置和插件等)都存储在 git 中。这样,重新构建服务器只需要执行一条命令即可完成。

        结语

        实际上,构建流程只包含三个阶段:获取代码 → 检测变更 → 部署。但真正的生产环境部署往往涉及到许多复杂的配置细节,比如反向代理、证书设置、绑定挂载、凭证管理以及时区设置等。这些配置虽然并不复杂,但它们的组合却会直接影响部署操作的顺利进行与否。

        按照第 1 至 11 节的内容来搭建一个可正常使用的构建流程;同时将第 12 节的内容添加书签,以确保这个流程能够长期稳定地运行。

        祝您的部署工作顺利成功!

Comments are closed.