GoCD Docker Compose 部署指南
以 Docker Compose 在单机部署 GoCD 26.1.0 完整环境(server+agent):镜像选型与 tag 坑、数据目录属主、管理员账号创建、agent 注册授权、最小流水线真实触发跑绿、JVM 堆限验证、持久化重启恢复、等价 docker run 与排障 FAQ,全部命令逐条实测。
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 介绍与选型指南》。
一、环境信息
本章记录实测服务器的软硬件基准与端口/网段占用现状,是后续端口与子网取值的事实依据。
| 项目 | 值 | 说明 |
|---|---|---|
| 服务器 IP | 192.168.146.128(ens33) | ip -4 addr show ens33 实测 |
| 操作系统 | Ubuntu 22.04.4 LTS(内核 6.8.0-138-generic) | PRETTY_NAME/uname -r 实测 |
| Docker | 29.8.1 | docker version --format '{{.Server.Version}}' 实测 |
| Docker Compose | v5.5.1 | docker 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):
- 镜像 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
- agent 镜像必须用带 OS 变体的名称:
gocd/gocd-agent名下无v26.1.0tag,需用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 对应,镜像与源码版本一致。
三、前置条件
- Linux 服务器一台(本文 Ubuntu 22.04.4),已安装 Docker 29.x 与 Docker Compose v2+(
docker compose version可用); - 端口规划:宿主机 8146(复用为 Web/API 入口,映射到服务 8153),部署前用
ss -ltn复核空闲; - 网络规划:专用 bridge 网络使用 10.89.63.0/24(避开 192.168.0.0/16 与 100.64.0.0/16);
- 磁盘空间:两镜像合计约 1.1 GB,数据目录预留 5 GB 以上;
- 服务器已安装 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
逐项说明:
| 配置项 | 取值 | 说明 |
|---|---|---|
name | gocd-26.1.0-stack | compose 项目名显式声明,避免与同机其他 compose 项目同名互删 |
ports | 8146:8153 | 宿主 8146 → 服务 8153(HTTP)。只映射 8153 即可:26.1.0 server 镜像内服务仅监听 8153,8154(HTTPS)未启用,映射 8154 也不会通 |
GOCD_SERVER_JVM_OPTS | -Xms256m -Xmx512m | server JVM 堆限制,256~512MB 区间(详见第六章) |
GO_SERVER_URL | http://gocd-server:8153/go | agent 连接 server 的入口;gocd-server 为同网络服务名(DNS 别名)。必须 http 且端口 8153 |
GOCD_AGENT_JVM_OPTS | -Xms256m -Xmx512m | agent JVM 堆限制 |
volumes | /opt/data/gocd/godata:/godata 等 | server 与 agent 各自持久化目录;两者都挂到服务内 /godata |
ipam.subnet | 10.89.63.0/24 | 显式子网,静态分配 .10/.11 两个地址 |
restart | unless-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 -Xmx512m | server JVM 启动参数,堆内存在此设置 |
GO_SERVER_SYSTEM_PROPERTIES | 未使用 | 旧式系统属性变量;⚠️ 实测对 26.1.0 镜像不能用于设置堆参数——堆参数必须走 GOCD_SERVER_JVM_OPTS(见下文验证) |
6.2 agent 环境变量
| 变量 | 实测取值 | 作用 |
|---|---|---|
GO_SERVER_URL | http://gocd-server:8153/go | server 接入地址;26.1.0 镜像 server 仅监听 8153(HTTP),写 8154 会连接拒绝(FAQ Q3) |
GOCD_AGENT_JVM_OPTS | -Xms256m -Xmx512m | agent 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,首次启动自动生成 |
tokenGenerationKey | agent 注册 token 的签名密钥,自动生成 |
webhookSecret | SCM webhook 请求签名密钥,自动生成 |
agentAutoRegisterKey | agent 自动注册密钥: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 节均验证):
- 直接编辑宿主机
/opt/data/gocd/godata/config/cruise-config.xml; - 数秒后日志出现
Config file changed at ...与Finished notifying all listeners(加载成功); - 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 怎么办?
三层排障(均实测):
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。- 卡在
auth.docker.io/token超时 → Docker Hub 不可达。为 daemon 配置镜像加速或 HTTP 代理后重试。 - 加速地址也断流(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 等)。
参考链接
- GoCD 官网:
https://www.gocd.org - 官方文档站:
https://docs.gocd.org/current/ - 官方 Docker 安装文档:
https://docs.gocd.org/current/installation/install_in_docker.html - server 镜像页:
https://hub.docker.com/r/gocd/gocd-server - agent 镜像页:
https://hub.docker.com/r/gocd/gocd-agent-ubuntu-24.04 - GoCD REST API 文档:
https://api.gocd.org/26.1.0/ - 密码文件认证插件:
https://github.com/gocd/gocd-ldap-authentication(同仓库系列 passwordfile 插件说明) - 配置参考(cruise-config schema):
https://docs.gocd.org/current/configuration/configuration_reference.html - 平台同类组件部署实战:《Woodpecker CI Docker Compose 部署指南》