GoCD Docker Compose 部署指南

以 Docker Compose 在单机部署 GoCD 26.1.0 完整环境(server+agent):镜像选型与 tag 坑、数据目录属主、管理员账号创建、agent 注册授权、最小流水线真实触发跑绿、JVM 堆限验证、持久化重启恢复、等价 docker run 与排障 FAQ,全部命令逐条实测。

GoCDGoCDCI/CD2026/10/20

GoCD Docker Compose 部署指南

目标:在单台 Linux 服务器上以 Docker Compose 部署 GoCD 26.1.0 完整环境(1 个 server + 1 个 agent),完成管理员账号创建、agent 注册授权、最小流水线创建并真实触发跑绿(stage/job Passed),最后给出等价手工启动方式、数据持久化验证与完整卸载步骤。本文全部命令逐条实测,回显照抄记录(实测日期 2026-10-20,实测版本 v26.1.0)。选型背景见《GoCD 26.1.0 介绍与选型指南》。

一、环境信息

本章记录实测服务器的软硬件基准与端口/网段占用现状,是后续端口与子网取值的事实依据。

项目值说明
服务器 IP192.168.146.128(ens33)ip -4 addr show ens33 实测
操作系统Ubuntu 22.04.4 LTS(内核 6.8.0-138-generic)PRETTY_NAME/uname -r 实测
Docker29.8.1docker version --format '{{.Server.Version}}' 实测
Docker Composev5.5.1docker compose version 实测
GoCD 版本26.1.0(2026-07-06 发布,官方最新稳定版)GitHub Releases 实查,2026-10-20
编排目录/opt/data/ops/gocd本文部署文件所在目录
数据目录/opt/data/gocd(server 与 agent 各一个子目录)bind mount 持久化

端口占用复核(ss -ltn 实测监听端口清单,2026-10-20):

22 53 80 631 3128 3306 5432 6010 6081 6082 6379 8080 8144 8145 8148 33055 33060 43709 51000

8144、8145、8148 已被同机其他服务占用。按规范在 8128~8152 区间顺延取空闲者:8146 → GoCD Server Web/API(8146 复核空闲)。

子网占用摸底(docker network inspect 逐个实测):

directus-12-5_directus-net -> 10.89.33.0/24
beanstalkd-net             -> 10.89.56.0/24
concourse-831-net          -> 10.89.58.0/24
pyroscope-2-4-stack_pyroscope-net -> 10.89.62.0/24

10.89.56~75 槽位中 56、58、62 已被占用,本文创建专用网络取空闲槽位 10.89.63.0/24。

二、镜像选型说明

GoCD 官方在 Docker Hub 发布两族镜像:

镜像角色说明
gocd/gocd-server调度中心Web UI、流水线调度、配置中心、制品仓库;26.1.0 版镜像基于 Chainguard apko 构建(distroless 风格),内置 JVM,进程以 go 用户(uid=1000,gid=0)运行
gocd/gocd-agent-ubuntu-24.04执行节点实际执行 job 的工作进程;agent 镜像按操作系统变体发布(ubuntu-24.04 / alpine 等),内置 git、JVM 等任务依赖

版本与 tag 的两个关键实测结论(2026-10-20):

  1. 镜像 tag 带 v 前缀:GoCD 源码 tag 是 26.1.0,但 Docker Hub 镜像 tag 是 v26.1.0。直接 docker pull gocd/gocd-server:26.1.0 会得到 denied(tag 不存在)。实测回显:
denied: requested access to the resource is denied
  1. agent 镜像必须用带 OS 变体的名称:gocd/gocd-agent 名下无 v26.1.0 tag,需用 gocd/gocd-agent-ubuntu-24.04:v26.1.0。

镜像获取标准命令:

docker pull gocd/gocd-server:v26.1.0
docker pull gocd/gocd-agent-ubuntu-24.04:v26.1.0

⚠️ 实测当日 Docker Hub 出现持续性网络故障(auth.docker.io 超时、blob 下载 EOF),标准 pull 无法完成。排障过程与手动拉取兜底方案见「十一、常见问题」Q1——该场景不影响正常网络环境下的读者,正常执行上面两条命令即可。

镜像到位后确认版本号(镜像内自报版本与 REST API 返回一致,实测于部署完成后):

curl -s -u admin:q1w2e3r4 http://localhost:8146/go/api/version -H 'Accept: application/vnd.go.cd.v1+json'
{
  "_links" : {
    "self" : {
      "href" : "http://localhost:8146/go/api/version"
    },
    "doc" : {
      "href" : "https://api.gocd.org/26.1.0/#version"
    }
  },
  "version" : "26.1.0",
  "build_number" : "22803",
  "git_sha" : "55b7b460510bb739c1ae6d226ff7fb650596dca2",
  "full_version" : "26.1.0 (22803-55b7b460510bb739c1ae6d226ff7fb650596dca2)",
  "commit_url" : "https://github.com/gocd/gocd/commit/55b7b460510bb739c1ae6d226ff7fb650596dca2"
}

version: 26.1.0、build_number: 22803 与 GitHub Release 对应,镜像与源码版本一致。

三、前置条件

  1. Linux 服务器一台(本文 Ubuntu 22.04.4),已安装 Docker 29.x 与 Docker Compose v2+(docker compose version 可用);
  2. 端口规划:宿主机 8146(复用为 Web/API 入口,映射到服务 8153),部署前用 ss -ltn 复核空闲;
  3. 网络规划:专用 bridge 网络使用 10.89.63.0/24(避开 192.168.0.0/16 与 100.64.0.0/16);
  4. 磁盘空间:两镜像合计约 1.1 GB,数据目录预留 5 GB 以上;
  5. 服务器已安装 git 与 python3(4.5 节生成密码文件、4.7 节创建流水线物料仓库会用到;仅本机操作,非服务依赖)。

💡 技术拓展:为什么 agent 不需要开放入站端口。GoCD 采用「agent 主动长轮询」模型——agent 启动后主动向 server 的 /go 端点发起 HTTP 长轮询领取任务,server 从不反向连接 agent。因此 agent 侧无需任何端口映射,天然适配 NAT/防火墙环境。这也是它与「server 反连 agent」(如 Jenkins SSH agent)的关键差异。

四、部署步骤

4.1 创建编排目录与数据目录

sudo mkdir -p /opt/data/ops/gocd
sudo mkdir -p /opt/data/gocd/godata /opt/data/gocd/agent-godata
sudo chown -R "$(id -un)":"$(id -gn)" /opt/data/ops/gocd
cd /opt/data/ops/gocd

关键一步——数据目录属主:26.1.0 server/agent 镜像内部以 go 用户(uid=1000,gid=0)运行,bind mount 目录若属主是 root,进程写日志/数据库会报 mkdir: Permission denied 并反复重启。实测将属主改为 1000:0:

sudo chown -R 1000:0 /opt/data/gocd/godata /opt/data/gocd/agent-godata

4.2 编写 docker-compose.yml

新建 /opt/data/ops/gocd/docker-compose.yml:

name: gocd-26.1.0-stack

services:
  gocd-server:
    image: gocd/gocd-server:v26.1.0
    container_name: gocd-26.1.0-server
    hostname: gocd-server
    ports:
      - "8146:8153"    # Web UI 与 agent 接入(HTTP)
    environment:
      GOCD_SERVER_JVM_OPTS: "-Xms256m -Xmx512m"
    volumes:
      - /opt/data/gocd/godata:/godata
    networks:
      gocd-net:
        ipv4_address: 10.89.63.10
    restart: unless-stopped

  gocd-agent:
    image: gocd/gocd-agent-ubuntu-24.04:v26.1.0
    container_name: gocd-26.1.0-agent
    hostname: gocd-agent
    environment:
      GO_SERVER_URL: "http://gocd-server:8153/go"
      GOCD_AGENT_JVM_OPTS: "-Xms256m -Xmx512m"
    volumes:
      - /opt/data/gocd/agent-godata:/godata
    depends_on:
      - gocd-server
    networks:
      gocd-net:
        ipv4_address: 10.89.63.11
    restart: unless-stopped

networks:
  gocd-net:
    driver: bridge
    name: gocd-net
    ipam:
      config:
        - subnet: 10.89.63.0/24

逐项说明:

配置项取值说明
namegocd-26.1.0-stackcompose 项目名显式声明,避免与同机其他 compose 项目同名互删
ports8146:8153宿主 8146 → 服务 8153(HTTP)。只映射 8153 即可:26.1.0 server 镜像内服务仅监听 8153,8154(HTTPS)未启用,映射 8154 也不会通
GOCD_SERVER_JVM_OPTS-Xms256m -Xmx512mserver JVM 堆限制,256~512MB 区间(详见第六章)
GO_SERVER_URLhttp://gocd-server:8153/goagent 连接 server 的入口;gocd-server 为同网络服务名(DNS 别名)。必须 http 且端口 8153
GOCD_AGENT_JVM_OPTS-Xms256m -Xmx512magent JVM 堆限制
volumes/opt/data/gocd/godata:/godata 等server 与 agent 各自持久化目录;两者都挂到服务内 /godata
ipam.subnet10.89.63.0/24显式子网,静态分配 .10/.11 两个地址
restartunless-stopped异常退出自动拉起

4.3 启动

cd /opt/data/ops/gocd
docker compose up -d

实测回显:

 Container gocd-26.1.0-server Creating
 Container gocd-26.1.0-server Created
 Container gocd-26.1.0-agent Creating
 Container gocd-26.1.0-agent Created
 Container gocd-26.1.0-server Starting
 Container gocd-26.1.0-server Started
 Container gocd-26.1.0-agent Starting
 Container gocd-26.1.0-agent Started

确认运行状态:

docker compose ps
NAME                 IMAGE                                  COMMAND                   SERVICE       CREATED          STATUS          PORTS
gocd-26.1.0-agent    gocd/gocd-agent-ubuntu-24.04:v26.1.0   "tini-static -g -- /…"   gocd-agent    59 seconds ago   Up 58 seconds
gocd-26.1.0-server   gocd/gocd-server:v26.1.0               "tini-static -g -- /…"   gocd-server   59 seconds ago   Up 58 seconds   0.0.0.0:8146->8153/tcp, [::]:8146->8153/tcp

验证网络与静态 IP:

docker network inspect gocd-net --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'
10.89.63.0/24

server 启动需要 30~60 秒(JVM 初始化 + 数据库建表),期间 agent 会反复重试连接,属正常现象。

4.4 就绪验证与 Web 首访

等待约 1 分钟后,用登录页探活:

curl -sI http://localhost:8146/go/auth/login | head -5
HTTP/1.1 200 OK
Date: Tue, 20 Oct 2026 07:09:49 GMT
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN

返回 200 即 server 就绪。浏览器打开 http://<服务器IP>:8146/go 进入 Web UI——全新安装默认未启用安全认证,首次访问以匿名管理员身份进入;下一步先开启认证再创建管理员账号。

4.5 创建管理员账号(密码文件认证)

GoCD 通过认证插件管理账号。官方内置 cd.go.authentication.passwordfile 插件支持 bcrypt 密码文件,零外部依赖,适合起步与中小团队。步骤分两半:生成密码文件 + 在中心配置中启用。

第一步:生成 bcrypt 密码文件。用 python3 的 bcrypt 库生成 $2a$ 前缀哈希(GoCD 内置 jBcrypt 仅识别 $2a$,python bcrypt 默认生成的 $2b$ 前缀需通过 prefix=b"2a" 参数指定):

python3 -c "import bcrypt; print(bcrypt.hashpw(b'q1w2e3r4', bcrypt.gensalt(rounds=10, prefix=b'2a')).decode())"
$2a$10$rDlCydKPpOrf2GQukX9A/O8N.Cck/4rbU45bskdUW/RNziBxuusje

⚠️ bcrypt 每次生成的哈希都不同(盐随机),你的输出与上面不一致属正常,只要格式为 $2a$10$ 开头的 60 字符串即可。

将 admin:<哈希> 写入 server 数据目录的 config/password.properties(服务内路径为 /godata/config/password.properties):

printf 'admin:%s\n' '$2a$10$rDlCydKPpOrf2GQukX9A/O8N.Cck/4rbU45bskdUW/RNziBxuusje' | sudo tee /opt/data/gocd/godata/config/password.properties
sudo chown 1000:0 /opt/data/gocd/godata/config/password.properties

第二步:在中心配置文件中启用 security 节。编辑 /opt/data/gocd/godata/config/cruise-config.xml,在 <server ...> 元素内部加入(完整文件结构见第七章):

<security>
  <authConfigs>
    <authConfig id="9f2b1c3d4e5f6a7b8c9d0e1f2a3b4c5d" pluginId="cd.go.authentication.passwordfile">
      <property>
        <key>PasswordFilePath</key>
        <value>/godata/config/password.properties</value>
      </property>
    </authConfig>
  </authConfigs>
  <admins>
    <user>admin</user>
  </admins>
</security>

逐项说明:

配置项含义
authConfig pluginId认证插件 ID,密码文件插件固定为 cd.go.authentication.passwordfile
PasswordFilePath密码文件路径,以 server 进程视角书写(compose 挂载后即 /godata/config/password.properties)
admins/user超级管理员用户名,与密码文件中的用户名 admin 对应

保存后 server 在数秒内自动检测到配置变更并热加载(无需重启),日志可见:

2026-10-20 06:50:38,054 [configFileReload-1] INFO  go.config.ConfigAwareMessageAgent:95 - Config file changed at 2026-10-20T06:50:37.085Z

第三步:验证认证生效:

# 正确密码 → 200
curl -s -o /dev/null -w '%{http_code}\n' -u admin:q1w2e3r4 http://localhost:8146/go/api/version -H 'Accept: application/vnd.go.cd.v1+json'
200
# 错误密码 → 401
curl -s -w '\nhttp=%{http_code}\n' -u admin:wrongpass http://localhost:8146/go/api/agents -H 'Accept: application/vnd.go.cd.v7+json'
{
  "message": "Invalid credentials. Either your username and password are incorrect, or there is a problem with your browser cookies. Please check with your administrator."
}
http=401

浏览器访问 http://<服务器IP>:8146/go/auth/login,用 admin / q1w2e3r4 登录成功,管理员账号创建完成。

4.6 agent 注册与授权

agent 首次接入 server 时处于 Pending 状态,需要在 UI(Admin → Agents)或 API 上授权 enable 后才会领取任务。先查 agent 列表拿到 uuid:

curl -s -u admin:q1w2e3r4 http://localhost:8146/go/api/agents -H 'Accept: application/vnd.go.cd.v7+json' | python3 -m json.tool | head -30
{
    "_links": {
        "self": {
            "href": "http://localhost:8146/go/api/agents"
        },
        "doc": {
            "href": "https://api.gocd.org/26.1.0/#agents"
        },
        "find": {
            "href": "http://localhost:8146/go/api/agents/:uuid"
        }
    },
    "_embedded": {
        "agents": [
            {
                "uuid": "701e7549-18d9-49b1-bf9f-5778fadea2c1",
                "hostname": "gocd-agent",
                "ip_address": "10.89.63.11",
                "sandbox": "/go-working-dir",
                "agent_state": "Idle",
                "agent_config_state": "Pending",
                ...
            }
        ]
    }
}

agent_config_state: Pending 表示待授权。用 PATCH API 授权(v7 版本头;If-Match: * 跳过并发检查):

curl -s -w '\nhttp=%{http_code}\n' -X PATCH -u admin:q1w2e3r4 \
  http://localhost:8146/go/api/agents/701e7549-18d9-49b1-bf9f-5778fadea2c1 \
  -H 'Accept: application/vnd.go.cd.v7+json' -H 'If-Match: *' \
  -H 'Content-Type: application/json' \
  -d '{"agent_config_state": "Enabled"}'
{
  "_links" : {
    "self" : {
      "href" : "http://localhost:8146/go/api/agents/701e7549-18d9-49b1-bf9f-5778fadea2c1"
    },
    ...
  },
  "uuid" : "701e7549-18d9-49b1-bf9f-5778fadea2c1",
  "hostname" : "gocd-agent",
  ...
}
http=200

再次确认状态变为 Enabled + Idle:

curl -s -u admin:q1w2e3r4 http://localhost:8146/go/api/agents -H 'Accept: application/vnd.go.cd.v7+json' | python3 -c "import json,sys; d=json.load(sys.stdin); print(json.dumps([{'hostname':a['hostname'],'agent_config_state':a['agent_config_state'],'agent_state':a['agent_state']} for a in d['_embedded']['agents']]))"
[{"hostname": "gocd-agent", "agent_config_state": "Enabled", "agent_state": "Idle"}]

此时 Web UI 的 Agents 页面亦显示 gocd-agent 为 Enabled/Idle,执行节点接入完成。

⚠️ 若 agent 一直 Pending 且 UI 上 enable 后仍 403,多为 agent 残留了「安全认证启用前」的旧注册 token。处理:停 agent → 删除 agent 数据目录下 config/token 文件 → 启动 agent 重新注册(详见 FAQ Q4)。

4.7 创建最小流水线并真实触发

GoCD 的流水线必须绑定一个版本库物料(Git/SVN 等),本节用本地 bare 仓库做最小演示物料,验证「物料检出 → 任务执行 → stage/job 跑绿」闭环。

第一步:在 server 数据目录创建演示用 bare 仓库(服务内视角 /godata/materials/demo-app.git):

cd /opt/data/gocd/godata
git init --bare materials/demo-app.git
cd /tmp && rm -rf demo-app && mkdir demo-app && cd demo-app
git init -b main
echo "hello gocd demo v1" > app.txt
git add app.txt && git commit -m "initial commit"
git remote add origin /opt/data/gocd/godata/materials/demo-app.git
git push origin main

实测回显(commit 与 push 段):

[main (root-commit) bef2ada] initial commit
 Author: root <myquzuo@163.com>
 1 file changed, 1 insertion(+)
 create mode 100644 app.txt
To /opt/data/gocd/godata/materials/demo-app.git
 * [new branch]      main -> main

记录提交哈希 bef2adaf466c3d10d0d932973ad31c0e448fe67e(后文日志中核对)。

第二步:在 cruise-config.xml 中追加 pipelines 节。编辑 /opt/data/gocd/godata/config/cruise-config.xml,在 </server> 之后、</cruise> 之前加入:

<pipelines>
  <pipeline name="hello-cd">
    <timer>0 0 5 ? * *</timer>
    <materials>
      <git url="/godata/materials/demo-app.git" branch="main" />
    </materials>
    <stage name="build-stage">
      <jobs>
        <job name="run-echo">
          <tasks>
            <exec command="echo">
              <arg>hello-from-gocd</arg>
              <runif status="passed" />
            </exec>
          </tasks>
        </job>
      </jobs>
    </stage>
  </pipeline>
</pipelines>

逐项说明:

元素含义关键实测约束
<pipeline name>流水线名,API/UI 均以该名引用
<timer>定时触发(Quartz cron),0 0 5 ? * * = 每日 05:00必须写在 <materials> 之前——XSD 校验顺序为 params → trackingtool → timer → environmentvariables → dependencies → materials → stage,位置错误会导致整个配置加载失败并触发自动回滚
<git url>Git 物料;中心化配置下 url 以 server 进程视角书写挂载目录下路径即 /godata/materials/demo-app.git
<stage name>阶段,串行执行;默认自动通过(无需人工审批)人工审批见 <approval type="manual" />
<job name>作业,stage 内并行执行,调度到某台 agent
<exec command>shell 命令任务,<arg> 逐个传参命令以 agent 上的工作目录为 cwd 执行

注意:<timer> 与 <git> 同时存在时,流水线既有定时触发又有物料变更触发——实际观察中,物料出现新提交时立即触发(实测第一次运行即由物料变更自动触发,见下方 history 的 trigger_message)。

保存后等待热加载(数秒),用 API 确认流水线已加载(pipeline_groups 接口):

curl -s -u admin:q1w2e3r4 http://localhost:8146/go/api/admin/pipeline_groups/defaultGroup -H 'Accept: application/vnd.go.cd.v1+json' | python3 -m json.tool | grep -A3 '"pipelines"'
{
    "name": "defaultGroup",
    "pipelines": [
        "hello-cd"
    ],
    ...
}

第三步:手动触发运行。用调度 API 触发——必须携带 X-GoCD-Confirm: true 头,否则返回 400:

# 不带确认头 → 400
curl -s -w '\nhttp=%{http_code}\n' -X POST -u admin:q1w2e3r4 \
  http://localhost:8146/go/api/pipelines/hello-cd/schedule \
  -H 'Accept: application/vnd.go.cd.v1+json'
{
  "message" : "Missing required header 'X-GoCD-Confirm' with value 'true'"
}
http=400
# 带确认头 → 202 受理
curl -s -w '\nhttp=%{http_code}\n' -X POST -u admin:q1w2e3r4 \
  http://localhost:8146/go/api/pipelines/hello-cd/schedule \
  -H 'Accept: application/vnd.go.cd.v1+json' -H 'X-GoCD-Confirm: true'
{
  "message" : "Request to schedule pipeline hello-cd accepted"
}
http=202

第四步:查询运行结果。等 15 秒左右查 history:

curl -s -u admin:q1w2e3r4 http://localhost:8146/go/api/pipelines/hello-cd/history -H 'Accept: application/vnd.go.cd.v1+json' | python3 -m json.tool | grep -E '"counter"|"result"|"state"|"trigger_message"|"name"'

输出节选(实测,两次运行均绿):

{
    "name": "hello-cd",
    "counter": 2,
    "trigger_message": "Forced by admin",
    "stages": [
        {
            "result": "Passed",
            "status": "Passed",
            "name": "build-stage",
            "jobs": [
                {
                    "name": "run-echo",
                    "state": "Completed",
                    "result": "Passed"
                }
            ]
        }
    ]
},
{
    "name": "hello-cd",
    "counter": 1,
    "trigger_message": "modified by ops <ops@quzuo.net>",
    ...
}

counter 1 由物料变更自动触发(approver: changes),counter 2 由 API 手动触发(Forced by admin),两次 build-stage/run-echo 均为 Passed。

第五步:下载 job 控制台日志,确认命令真实执行(job 制品接口直接输出日志原文):

curl -s -u admin:q1w2e3r4 http://localhost:8146/go/files/hello-cd/2/build-stage/1/run-echo/cruise-output/console.log | tail -12
##|06:56:16.434 [go] Start to build hello-cd/2/build-stage/1/run-echo on gocd-agent [/go-working-dir]
!!|06:56:16.443 [go] Task: echo hello-from-gocd
&1|06:56:16.445 hello-from-gocd
?0|06:56:16.495 [go] Task status: passed (52 ms)
j0|06:56:16.507 [go] Current job status: passed
ar|06:56:16.515 [go] Start to upload hello-cd/2/build-stage/1/run-echo on gocd-agent [/go-working-dir]
ex|06:56:16.515 [go] Job completed hello-cd/2/build-stage/1/run-echo on gocd-agent [/go-working-dir]

Task: echo hello-from-gocd 与输出 hello-from-gocd、Task status: passed 完整闭环——server 调度、agent 检出物料(日志前段有 [GIT] Updating working copy to revision bef2ada...,与 4.7 第一步提交哈希一致)、任务执行、状态回报全部打通。部署验证完成。

五、数据持久化说明

server 与 agent 的全部状态都落在各自 /godata 目录(宿主机 /opt/data/gocd/godata 与 /opt/data/gocd/agent-godata)。实测目录结构(ls -la /opt/data/gocd/godata/):

total 32
drwxr-xr-x 8 yahui root 4096 10月 20 14:42 .
drwxr-xr-x 4 root  root 4096 10月 20 14:25 ..
drwxr-xr-x 3 yahui root 4096 10月 20 14:51 artifacts
drwxr-xr-x 2 yahui root 4096 10月 20 14:46 config
drwxr-xr-x 4 yahui root 4096 10月 20 14:26 db
drwxr-xr-x 2 yahui root 4096 10月 20 15:07 logs
drwxr-xr-x 3 yahui root 4096 10月 20 14:42 materials
drwxr-xr-x 4 yahui root 4096 10月 20 14:26 plugins
子目录内容备份优先级
config/cruise-config.xml 中心配置、密码文件、agent 自动注册密钥最高(小文件)
db/内嵌数据库(流水线历史、认证域、用户状态)最高
materials/物料缓存(本文演示仓库也在此)中(可重新克隆,缓存丢失只是变慢)
artifacts/job 制品与控制台日志按需
plugins/插件 jar低(随版本携带)
logs/服务日志低

重启恢复实测:docker compose down 后 docker compose up -d,重新就绪后三件事全部保持——认证仍生效(API 返回 200)、agent 无需重新授权(agent_config_state: Enabled、agent_state: Idle)、流水线历史完整(counter: 2、stage: Passed)。数据零丢失,验证通过。

六、常用环境变量表

6.1 server 环境变量

变量实测取值作用
GOCD_SERVER_JVM_OPTS-Xms256m -Xmx512mserver JVM 启动参数,堆内存在此设置
GO_SERVER_SYSTEM_PROPERTIES未使用旧式系统属性变量;⚠️ 实测对 26.1.0 镜像不能用于设置堆参数——堆参数必须走 GOCD_SERVER_JVM_OPTS(见下文验证)

6.2 agent 环境变量

变量实测取值作用
GO_SERVER_URLhttp://gocd-server:8153/goserver 接入地址;26.1.0 镜像 server 仅监听 8153(HTTP),写 8154 会连接拒绝(FAQ Q3)
GOCD_AGENT_JVM_OPTS-Xms256m -Xmx512magent JVM 启动参数

6.3 JVM 堆限制生效验证(实测)

用 ps aux 观察 server 进程完整命令行:

docker exec gocd-26.1.0-server sh -c "ps aux | grep '[j]ava' | head -2" | grep -oE '\-Xms[0-9]+m|\-Xmx[0-9]+m|\-XX:MaxMetaspaceSize=[0-9]+m'

输出节选(实测):

-Xms512m
-Xmx1024m
-XX:MaxMetaspaceSize=400m
-Xms256m
-Xmx512m

解读:镜像 wrapper 配置内置默认 -Xms512m -Xmx1024m 在前,我们注入的 -Xms256m -Xmx512m 追加在后——JVM 同参数后者覆盖前者,实际生效 256m/512m,符合 256~512MB 的堆限目标。

agent 侧同样验证(agent 实际工作进程由 bootstrapper 拉起,取第三条 java 进程):

docker exec gocd-26.1.0-agent sh -c "ps aux | grep '[j]ava'" | tail -1 | grep -oE '\-Xms[0-9]+m|\-Xmx[0-9]+m'
-Xms128m
-Xmx256m
-Xms256m
-Xmx512m

同样内置 -Xms128m -Xmx256m 被注入的 -Xms256m -Xmx512m 覆盖,实际生效 256m/512m。同时该进程命令行尾部的 -serverUrl http://gocd-server:8153/go 印证 GO_SERVER_URL 已正确传递给 agent。

七、常用配置参数说明(cruise-config.xml)

GoCD 的全部核心配置集中在 server 数据目录的 config/cruise-config.xml(服务内 /godata/config/cruise-config.xml)。本文实测完成的文件全文:

<?xml version="1.0" encoding="utf-8"?>
<cruise xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="cruise-config.xsd" schemaVersion="139">
  <server agentAutoRegisterKey="75f89f53-bea5-4bcd-b05e-a25d18365ee9" webhookSecret="42886f95-d960-445e-8432-f6931466e057" serverId="fdb12fb8-532d-4c8c-87f8-6947361dd82d" tokenGenerationKey="96ee4c80-8397-43c1-aca7-7ea6ef206721">
    <security>
      <authConfigs>
        <authConfig id="9f2b1c3d4e5f6a7b8c9d0e1f2a3b4c5d" pluginId="cd.go.authentication.passwordfile">
          <property>
            <key>PasswordFilePath</key>
            <value>/godata/config/password.properties</value>
          </property>
        </authConfig>
      </authConfigs>
      <admins>
        <user>admin</user>
      </admins>
    </security>
    <artifacts>
      <artifactsDir>artifacts</artifactsDir>
    </artifacts>
  </server>
  <pipelines>
    <pipeline name="hello-cd">
      <timer>0 0 5 ? * *</timer>
      <materials>
        <git url="/godata/materials/demo-app.git" branch="main" />
      </materials>
      <stage name="build-stage">
        <jobs>
          <job name="run-echo">
            <tasks>
              <exec command="echo">
                <arg>hello-from-gocd</arg>
                <runif status="passed" />
              </exec>
            </tasks>
          </job>
        </jobs>
      </stage>
    </pipeline>
  </pipelines>
</cruise>

7.1 根元素与 server 元素

属性/元素说明
schemaVersion="139"配置结构版本号,由服务端自动维护,26.1.0 为 139,勿手改
serverId实例唯一 ID,首次启动自动生成
tokenGenerationKeyagent 注册 token 的签名密钥,自动生成
webhookSecretSCM webhook 请求签名密钥,自动生成
agentAutoRegisterKeyagent 自动注册密钥:server 侧声明后,agent 侧提供相同密钥(数据目录 config/autoregister.properties)即可跳过 Pending 审批自动注册;本文未在 agent 侧配置该密钥,故走「Pending → enable」审批流程(4.6 节)

7.2 security 节(认证与授权)

见 4.5 节逐项说明。补充两点实测结论:全新安装默认无 <security> 节(匿名管理员);加入后立即生效且历史匿名 token 失效——若 agent 在启用前已注册,需删 agent token 重新注册(FAQ Q4)。

7.3 pipeline 元素顺序(XSD 校验,实测踩坑)

<pipeline> 子元素有严格顺序(由镜像内 cruise-config.xsd 定义),实测顺序为:

params? → trackingtool? → timer? → environmentvariables? → dependencies* → materials → stage+

顺序错误时配置不加载,且 server 会把无效文件改名保留为 cruise-config.xml.invalid.<时间戳> 后回滚到上一有效版本——首次启动校验失败时数据目录会出现该文件,可从中看报错原因。timer 的两种常见错误位置(materials 之后、stage 之后)实测均导致校验失败。

八、配置文件挂载与热加载

cruise-config.xml 无需重启即可生效:server 每 5 秒轮询文件变更,检测到修改后校验 + 热加载。实测流程(4.5、4.7 节均验证):

  1. 直接编辑宿主机 /opt/data/gocd/godata/config/cruise-config.xml;
  2. 数秒后日志出现 Config file changed at ... 与 Finished notifying all listeners(加载成功);
  3. API 立即可查到新配置(4.7 节 pipeline_groups 回显)。

校验失败时的行为(实测):文件被改名保留为 cruise-config.xml.invalid.<时间戳>,server 继续用上一份有效配置运行,日志给出 XSD 报错位置。这一机制保证「改错配置不炸服务」,但要求改完配置后必查日志或 API 确认加载成功,否则会一直用旧配置运行而不自知。

⚠️ 权限:编辑保存后的文件属主可能变回当前用户,server 进程(uid=1000)需能读写该文件,建议改完执行 sudo chown 1000:0 保持属主一致。

九、等价 docker run 运行方式

不用 Compose 时,以下手工命令与 4.2 节 compose 定义逐项等价(实测通过)。

创建同款网络:

docker network create -d bridge --subnet 10.89.64.0/24 gocd-run-net
b8da58664a11fdf43647965f2bd7bffde138c2465c9938238d53eb37abf3f164

启动 server(等价于 compose 的 gocd-server 定义,端口/环境变量/挂载/网络四要素一致):

docker run -d --name gocd-26.1.0-run-server --hostname gocd-server \
  -p 8147:8153 \
  -e GOCD_SERVER_JVM_OPTS="-Xms256m -Xmx512m" \
  -v /opt/data/quzuo/tmp/gocd/run-data/godata:/godata \
  --network gocd-run-net \
  gocd/gocd-server:v26.1.0
fe22f859d90baf0f22989d1895f47e4f3c966178696733c26226dbcc4ccd2188

启动 agent(等价于 compose 的 gocd-agent 定义):

docker run -d --name gocd-26.1.0-run-agent --hostname gocd-agent \
  -e GO_SERVER_URL="http://gocd-server:8153/go" \
  -e GOCD_AGENT_JVM_OPTS="-Xms256m -Xmx512m" \
  -v /opt/data/quzuo/tmp/gocd/run-data/agent-godata:/godata \
  --network gocd-run-net \
  gocd/gocd-agent-ubuntu-24.04:v26.1.0
5f60fbed4e2e835dba017336762bed78574ef3e8efd3dad6297aaed49f5ea6fb

等待约 45 秒后验证:Web 探活返回 200(新实例未启用认证,匿名可访问,访问需认证的 API 会返回如下提示)、agent 已注册进入列表:

curl -s -u admin:q1w2e3r4 http://localhost:8147/go/api/version -H 'Accept: application/vnd.go.cd.v1+json'
{
  "message": "Basic authentication credentials are not required, since security has been disabled on this server."
}
curl -s http://localhost:8147/go/api/agents -H 'Accept: application/vnd.go.cd.v7+json' | python3 -c "import json,sys; d=json.load(sys.stdin); print(json.dumps([{'uuid':a['uuid'],'hostname':a['hostname'],'agent_state':a['agent_config_state'],'status':a['agent_state'],'ip':a['ip_address']} for a in d['_embedded']['agents']], indent=1))"
[
 {
  "uuid": "f83d3301-8020-40b7-9d9f-599e9c541685",
  "hostname": "gocd-agent",
  "agent_state": "Pending",
  "status": "Unknown",
  "ip": "10.89.64.3"
 }
]

agent 出现且为 Pending(待授权),与 compose 方式行为一致,等价性验证通过。验证完成后清理:

docker rm -f gocd-26.1.0-run-server gocd-26.1.0-run-agent
docker network rm gocd-run-net
rm -rf /opt/data/quzuo/tmp/gocd/run-data

十、常用运维命令

操作命令
状态一览docker compose ps
server 日志跟踪docker compose logs -f gocd-server
agent 日志跟踪docker compose logs -f gocd-agent
重启单个服务docker compose restart gocd-server
全栈重启(数据保留)docker compose down && docker compose up -d
版本查询curl -s -u admin:q1w2e3r4 http://localhost:8146/go/api/version -H 'Accept: application/vnd.go.cd.v1+json'
agent 列表curl -s -u admin:q1w2e3r4 http://localhost:8146/go/api/agents -H 'Accept: application/vnd.go.cd.v7+json'
流水线暂停/恢复见下方命令块

流水线暂停与恢复(实测回显):

curl -s -w '\nhttp=%{http_code}\n' -X POST -u admin:q1w2e3r4 http://localhost:8146/go/api/pipelines/hello-cd/pause -H 'Accept: application/vnd.go.cd.v1+json' -H 'X-GoCD-Confirm: true' -H 'Content-Type: application/json' -d '{"pauseCause":"api-test"}'
{
  "message" : "Pipeline 'hello-cd' paused successfully."
}
http=200
curl -s -X POST -u admin:q1w2e3r4 http://localhost:8146/go/api/pipelines/hello-cd/unpause -H 'Accept: application/vnd.go.cd.v1+json' -H 'X-GoCD-Confirm: true' -w '\nhttp=%{http_code}\n'
{
  "message" : "Pipeline 'hello-cd' unpaused successfully."
}
http=200

升级版本:修改 compose 中两处 image tag(server 与 agent 需同版本)→ docker compose up -d 滚动重建;数据目录 schema 由服务端自动迁移,先备份 /opt/data/gocd 再操作。

十一、常见问题(FAQ)

Q1:docker pull 报 denied 或拉取超时/EOF 怎么办?

三层排障(均实测):

  1. denied: requested access to the resource is denied → tag 写错。官方镜像 tag 带 v 前缀,用 v26.1.0 而非 26.1.0;agent 用 gocd/gocd-agent-ubuntu-24.04:v26.1.0 而非 gocd/gocd-agent:v26.1.0。
  2. 卡在 auth.docker.io/token 超时 → Docker Hub 不可达。为 daemon 配置镜像加速或 HTTP 代理后重试。
  3. 加速地址也断流(blob 下载 EOF)→ 手动从 Registry 拉 OCI 布局再 docker load。实测使用以下脚本(保存为 pull_oci.py,用法 python3 pull_oci.py <镜像名>:<tag> <输出目录>,脚本内的 docker.1ms.run 为实测当日可用的加速地址,可按需替换为其他可用 Registry 镜像域名):
#!/usr/bin/env python3
# 从 docker.1ms.run 手动拉镜像:manifest -> blobs -> OCI layout tar -> docker load
import json, os, sys, subprocess, hashlib, time, urllib.request, urllib.error, shutil

REG = 'https://docker.1ms.run'

class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None

def http(url, headers=None, dest=None, retries=5):
    req = urllib.request.Request(url)
    req.add_header('User-Agent', 'curl/8.5.0')   # 部分 CDN 拦截 python-urllib 默认 UA
    for k, v in (headers or {}).items():
        req.add_header(k, v)
    last = None
    for i in range(retries):
        try:
            with urllib.request.urlopen(req, timeout=1200) as r:
                data = r.read()
            if dest:
                open(dest, 'wb').write(data)
                return len(data)
            return data
        except Exception as e:
            last = e
            time.sleep(3)
    raise RuntimeError(f'GET failed {url}: {last}')

def main(image, outdir):
    repo, tag = image.split(':')
    os.makedirs(f'{outdir}/blobs/sha256', exist_ok=True)
    tok = json.loads(http(f'{REG}/openapi/v1/auth/token?service=docker.1ms.run&scope=repository:{repo}:pull'))
    token = tok.get('token') or tok.get('access_token')
    H = {'Authorization': f'Bearer {token}'}
    ACC = ('application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, '
           'application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json')
    idx_raw = http(f'{REG}/v2/{repo}/manifests/{tag}', {**H, 'Accept': ACC})
    idx = json.loads(idx_raw)
    if 'manifests' in idx:  # index -> 取 amd64/linux
        man_digest = next(m['digest'] for m in idx['manifests']
                          if m.get('platform', {}).get('architecture') == 'amd64'
                          and m.get('platform', {}).get('os') == 'linux')
        man_raw = http(f'{REG}/v2/{repo}/manifests/{man_digest}', {**H, 'Accept': ACC})
    else:
        man_raw = idx_raw
        man_digest = 'sha256:' + hashlib.sha256(man_raw).hexdigest()
    man = json.loads(man_raw)
    cfg = man['config']['digest']
    layers = [l['digest'] for l in man['layers']]
    total = sum(l['size'] for l in man['layers'])
    print(f'image {image}: config {cfg[:20]}.. layers={len(layers)} total={total/1e6:.1f}MB', flush=True)

    def blob_path(d):
        return f"{outdir}/blobs/sha256/{d.split(':')[1]}"

    def fetch(d, what):
        p = blob_path(d)
        if os.path.exists(p) and os.path.getsize(p) > 0:
            print(f'  skip {what} {d[:20]}.. ({os.path.getsize(p)/1e6:.1f}MB cached)', flush=True)
            return
        tmp = p + '.part'
        for i in range(8):
            try:
                loc = None
                r = urllib.request.Request(f'{REG}/v2/{repo}/blobs/{d}', headers=H)
                r.add_header('User-Agent', 'curl/8.5.0')
                try:   # 30x 跳转的签名 URL 不能再带 Authorization,手动分离两次请求
                    with urllib.request.build_opener(NoRedirect).open(r, timeout=300) as resp:
                        if resp.status in (301, 302, 307, 308):
                            loc = resp.headers['Location']
                except urllib.error.HTTPError as e:
                    if e.code in (301, 302, 307, 308):
                        loc = e.headers['Location']
                    else:
                        raise
                if loc:
                    lr = urllib.request.Request(loc, headers={'User-Agent': 'curl/8.5.0'})
                    with urllib.request.urlopen(lr, timeout=1800) as resp, open(tmp, 'wb') as f:
                        shutil.copyfileobj(resp, f, 1 << 20)
                else:
                    with urllib.request.urlopen(r, timeout=1800) as resp, open(tmp, 'wb') as f:
                        shutil.copyfileobj(resp, f, 1 << 20)
                os.rename(tmp, p)
                print(f'  got {what} {d[:20]}.. ({os.path.getsize(p)/1e6:.1f}MB)', flush=True)
                return
            except Exception as e:
                print(f'  retry {i+1} {what} {d[:20]}..: {e}', flush=True)
                time.sleep(5)
        raise RuntimeError(f'blob fetch failed {d}')

    fetch(cfg, 'config')
    for i, d in enumerate(layers):
        fetch(d, f'layer{i+1}/{len(layers)}')

    # 组装 OCI layout(manifest 用原始字节保 digest 一致)并打成根级 tar 供 docker load
    open(f'{outdir}/oci-layout', 'w').write('{"imageLayoutVersion":"1.0.0"}')
    open(f'{outdir}/blobs/sha256/{man_digest.split(":")[1]}', 'wb').write(man_raw)
    index = {'schemaVersion': 2, 'mediaType': 'application/vnd.oci.image.index.v1+json',
             'manifests': [{'mediaType': 'application/vnd.oci.image.manifest.v1+json',
                            'digest': man_digest, 'size': os.path.getsize(blob_path(man_digest)),
                            'annotations': {'io.containerd.image.name': f'docker.io/{repo}:{tag}',
                                            'org.opencontainers.image.ref.name': f'{repo}:{tag}'}}]}
    open(f'{outdir}/index.json', 'w').write(json.dumps(index))
    tar = f'{outdir}.tar'
    subprocess.run(['tar', '-C', outdir, '-cf', tar, '.'], check=True)
    print(f'layout tar: {tar} ({os.path.getsize(tar)/1e6:.1f}MB)', flush=True)

if __name__ == '__main__':
    main(sys.argv[1], sys.argv[2])

拉取完成后导入(实测两镜像均成功 load):

docker load -i oci-server.tar
docker load -i oci-agent.tar

⚠️ 两个实测细节:tar 必须以布局目录为根打包(tar -C outdir -cf xxx.tar .),顶层多套一层目录会导致 docker load 报 unrecognized image format;manifest 文件必须保存 Registry 返回的原始字节,重新序列化会因键序/空白差异导致 digest 不匹配、load 后镜像 size 显示 0。读者网络正常时无需此兜底。

Q2:agent 反复重启,日志报 mkdir: Permission denied?

bind mount 目录属主是 root 所致。镜像内进程以 uid=1000(go 用户)运行,执行 sudo chown -R 1000:0 /opt/data/gocd/godata /opt/data/gocd/agent-godata 后重启栈即可。实测现象为 Restarting (111) 循环。

Q3:agent 连不上 server,日志报 Connection refused 且端口是 8154?

26.1.0 的 server 镜像内服务仅监听 8153(HTTP),未启用 8154(HTTPS)。GO_SERVER_URL 必须写 http://gocd-server:8153/go(实测将 8154 改为 8153 后 agent 立即注册成功)。compose 中也无需映射 8154。

Q4:agent 已在列表显示 Pending,enable 后仍不可用 / 403 pending approval?

agent 持有「security 启用前」注册的旧 token,security 生效后旧 token 失效。处理(实测有效):

cd /opt/data/ops/gocd
docker compose stop gocd-agent
sudo rm /opt/data/gocd/agent-godata/config/token
docker compose start gocd-agent

agent 以新 token 重新注册后,在 UI 或 4.6 节的 PATCH API 上 enable。

Q5:修改 cruise-config.xml 后服务重启且文件被改名?

XSD 校验失败。检查:<roles /> 空元素会报 One of '{baseRole}' is expected(删除空元素);<timer> 放在 <materials> 之后或 <stage> 之后会校验失败(必须在前,见 7.3 节顺序)。无效文件被保留为 cruise-config.xml.invalid.<时间戳>,从中可见报错行。

Q6:设置了 GO_SERVER_SYSTEM_PROPERTIES 但堆内存不生效?

26.1.0 镜像的堆参数入口是 GOCD_SERVER_JVM_OPTS(agent 同理为 GOCD_AGENT_JVM_OPTS)。实测旧变量不参与 JVM 启动参数拼接。设置后按 6.3 节用 ps aux 验证生效值。

Q7:触发流水线返回 400 Missing required header 'X-GoCD-Confirm'?

调度类写操作 API 必须显式携带确认头 -H 'X-GoCD-Confirm: true'(4.7 节实测对比)。

Q8:没有 Git 服务器,流水线物料怎么配?

与本文 4.7 节一致:在 server 数据目录 materials/ 下 git init --bare 建本地 bare 仓库,<git url="/godata/materials/demo-app.git" /> 即可(挂载路径视角)。真实团队环境将 url 换成正式 Git 仓库(GitHub/GitLab/Gitea 等)。

参考链接

  1. GoCD 官网:https://www.gocd.org
  2. 官方文档站:https://docs.gocd.org/current/
  3. 官方 Docker 安装文档:https://docs.gocd.org/current/installation/install_in_docker.html
  4. server 镜像页:https://hub.docker.com/r/gocd/gocd-server
  5. agent 镜像页:https://hub.docker.com/r/gocd/gocd-agent-ubuntu-24.04
  6. GoCD REST API 文档:https://api.gocd.org/26.1.0/
  7. 密码文件认证插件:https://github.com/gocd/gocd-ldap-authentication(同仓库系列 passwordfile 插件说明)
  8. 配置参考(cruise-config schema):https://docs.gocd.org/current/configuration/configuration_reference.html
  9. 平台同类组件部署实战:《Woodpecker CI Docker Compose 部署指南》