Concourse 8.3.1 Docker Compose 部署指南

在 Ubuntu 22.04 上用 Docker Compose 部署 Concourse 8.3.1 三服务栈(web+worker+PostgreSQL):组件密钥生成、编排文件、fly 登录、hello-world 流水线设置与触发、Web UI 验证与常见问题排查,全程实测。

Concourse2026/10/20

Concourse 8.3.1 Docker Compose 部署指南

本文以真实服务器视角记录使用 Docker Compose 部署 Concourse 8.3.1 三服务栈(web + worker + PostgreSQL)的全过程:组件密钥生成、编排文件编写、fly CLI 登录、hello-world 流水线设置与触发、Web UI 验证与常见问题排查,全部命令与输出均在 Ubuntu 22.04 实测环境中逐行照抄。

一、环境信息

  • 操作系统:Ubuntu 22.04.4 LTS(内核 6.8.0-138-generic,x86_64,12 核 / 32G 内存)
  • Docker 版本:29.8.1(docker --version 实测)
  • Docker Compose 版本:v5.5.1(docker compose version 实测)
  • Concourse 版本:8.3.1(官方最新稳定版,GitHub Releases 查询日期 2026-10-20)
  • 软件包版本:concourse/concourse:8.3.1(web 与 worker 共用)、postgres:16-alpine
  • 对外服务端口:8148(映射到 Concourse web 默认 8080)
  • 编排网络:自定义网络 10.89.58.0/24
  • 管理账号:root / q1w2e3r4(本地认证,main 团队管理员)
  • 编排文件:/opt/concourse/docker-compose.yml(项目名 concourse-831-stack)

二、架构与组件说明

Concourse 采用"Web 节点 + Worker 节点 + PostgreSQL"结构,本方案用三个服务在一个宿主机上组成最小可用集群:

服务软件包进程职责
dbpostgres:16-alpinepostgres唯一持久化存储:流水线配置、构建历史、资源版本、团队与账号
webconcourse/concourse:8.3.1concourse web(ATC + TSA)Web UI / REST API(8080)、调度构建、SSH worker 注册网关(2222)
workerconcourse/concourse:8.3.1concourse worker运行构建任务(containerd 运行时)、Baggageclaim 卷管理、beacon 心跳注册

三个服务来自同一份官方软件包 concourse/concourse:8.3.1,通过 command 区分进程角色(web / worker),这是 Concourse 单二进制分发的典型用法。web 与 worker 之间通过 TSA 的 SSH 通道通信,worker 必须持有正确的 RSA 密钥材料才能注册成功(见第四节)。

三、前置条件

3.1 Docker 与 Compose

docker --version
docker compose version

实测输出:

Docker version 29.8.1, build 4a63305
Docker Compose version v5.5.1

3.2 选择服务端口

Concourse web 默认监听 8080,需映射到宿主一个空闲端口。先查看已占用端口:

ss -ltn | awk '{print $4}' | grep -E ':(81[0-9][0-9])$'

实测输出:

0.0.0.0:8144
0.0.0.0:8145

8148 未被占用,本文采用 8148:8080 映射。

3.3 选择自定义网络子网

为避免与宿主机上其他在跑的编排栈冲突,先盘点已占用的自定义网段,再从空闲槽位中显式选取:

docker network inspect $(docker network ls -q) --format '{{.Name}} {{range .IPAM.Config}}{{.Subnet}}{{end}}' | sort

实测输出:

beanstalkd-net 10.89.56.0/24
bridge 10.0.0.0/24
directus-12-5_directus-net 10.89.33.0/24
host
none
pyroscope-2-4-stack_pyroscope-net 10.89.62.0/24
stack_default 10.0.1.0/24

10.89.58.0/24 空闲,作为本栈专用网段写入编排文件。

3.4 获取软件包

从官方制品库获取两个软件包(内网或网络受限环境可先在联网机器导出再导入):

docker pull concourse/concourse:8.3.1
docker pull postgres:16-alpine

💡 说明:concourse/concourse 官方软件包体量约 1GB,包含 web、worker 两类进程的可执行文件与 containerd/guardian 运行时;postgres:16-alpine 约 270MB。若官方源拉取缓慢,可改用可达的内网制品仓库代理地址。

四、生成组件密钥

Concourse 的 web 与 worker 之间通过 SSH 通道完成注册认证,最少需要三组密钥:

密钥类型生成方式使用方
session_signing_keyRSAssh-keygen -m PEM(或 generate-key -t rsa)web(签发用户会话令牌)
tsa_host_keySSHssh-keygen -m PEM(或 generate-key -t ssh)web 持私钥;worker 持公钥校验服务端
worker_keySSHssh-keygen -m PEM(或 generate-key -t ssh)worker 持私钥;公钥列入 web 的 authorized_worker_keys

创建目录并用宿主机 ssh-keygen 生成(官方文档同款参数:RSA 4096、PEM 编码;也可使用官方软件包自带的 generate-key 子命令,效果等价):

mkdir -p /opt/concourse/keys/web /opt/concourse/keys/worker
ssh-keygen -t rsa -b 4096 -m PEM -f /opt/concourse/keys/web/session_signing_key -N ''
ssh-keygen -t rsa -b 4096 -m PEM -f /opt/concourse/keys/web/tsa_host_key -N ''
ssh-keygen -t rsa -b 4096 -m PEM -f /opt/concourse/keys/worker/worker_key -N ''
cp /opt/concourse/keys/worker/worker_key.pub /opt/concourse/keys/web/authorized_worker_keys
cp /opt/concourse/keys/web/tsa_host_key.pub /opt/concourse/keys/worker/tsa_host_key.pub

实测输出(每条命令回显一致,摘录其一):

Generating public/private rsa key pair.
Your identification has been saved in /opt/concourse/keys/web/session_signing_key
Your public key has been saved in /opt/concourse/keys/web/session_signing_key.pub
The key fingerprint is:
SHA256:kSDPTuQ12ID+cqbciQYBBr5jd0PjISZRl5zcNkiGDFc root@ops
The key's randomart image is:
+---[RSA 4096]----+
|oo+oBEO+o        |
|o.oo+%.*.o       |
|.+ + +* +        |
|  = =oo  .       |
| + o =. S        |
|. + o =          |
|   o B .         |
|    = o          |
|   .             |
+----[SHA256]-----+

最终密钥目录结构:

/opt/concourse/keys/web/            # web 服务只读挂载
├── session_signing_key
├── tsa_host_key
├── tsa_host_key.pub
└── authorized_worker_keys          # 即 worker_key.pub 的副本

/opt/concourse/keys/worker/         # worker 服务只读挂载
├── worker_key
├── worker_key.pub
└── tsa_host_key.pub                # 必须有:worker 校验 TSA 服务端

⚠️ 实测踩坑:worker 侧目录若缺少 tsa_host_key.pub,进程将启动失败并不断重启,日志报 error: invalid argument for flag '--tsa-public-key' (expected flag.AuthorizedKeys): failed to read authorized keys: open /concourse-keys/tsa_host_key.pub: no such file or directory。

五、编写 docker-compose.yml

创建 /opt/concourse/docker-compose.yml(编排项目名必须显式指定,避免与其他在跑栈同名互删):

name: concourse-831-stack

services:
  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: concourse
      POSTGRES_USER: concourse_user
      POSTGRES_PASSWORD: concourse_pass
      PGDATA: /database
    volumes:
      - db-data:/database
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U concourse_user -d concourse"]
      interval: 5s
      timeout: 3s
      retries: 10

  web:
    image: concourse/concourse:8.3.1
    command: web
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "8148:8080"
    environment:
      CONCOURSE_CLUSTER_NAME: quzuo-lab
      CONCOURSE_EXTERNAL_URL: http://192.168.146.128:8148
      CONCOURSE_POSTGRES_HOST: db
      CONCOURSE_POSTGRES_PORT: "5432"
      CONCOURSE_POSTGRES_DATABASE: concourse
      CONCOURSE_POSTGRES_USER: concourse_user
      CONCOURSE_POSTGRES_PASSWORD: concourse_pass
      CONCOURSE_ADD_LOCAL_USER: root:q1w2e3r4
      CONCOURSE_MAIN_TEAM_LOCAL_USER: root
      CONCOURSE_SESSION_SIGNING_KEY: /concourse-keys/session_signing_key
      CONCOURSE_TSA_HOST_KEY: /concourse-keys/tsa_host_key
      CONCOURSE_TSA_AUTHORIZED_KEYS: /concourse-keys/authorized_worker_keys
    volumes:
      - ./keys/web:/concourse-keys:ro

  worker:
    image: concourse/concourse:8.3.1
    command: worker --containerd-dns-proxy-enable --containerd-dns-server=223.5.5.5
    restart: unless-stopped
    privileged: true
    stop_signal: SIGUSR2
    depends_on:
      - web
    environment:
      CONCOURSE_NAME: worker-01
      CONCOURSE_RUNTIME: containerd
      CONCOURSE_BAGGAGECLAIM_DRIVER: overlay
      CONCOURSE_TSA_HOST: web:2222
      CONCOURSE_TSA_PUBLIC_KEY: /concourse-keys/tsa_host_key.pub
      CONCOURSE_TSA_WORKER_PRIVATE_KEY: /concourse-keys/worker_key
    volumes:
      - ./keys/worker:/concourse-keys:ro

networks:
  default:
    name: concourse-831-net
    driver: bridge
    ipam:
      config:
        - subnet: 10.89.58.0/24

volumes:
  db-data:

要点说明:

  1. CONCOURSE_EXTERNAL_URL 写宿主机实际可达地址(浏览器与 fly 都经由它访问)。
  2. CONCOURSE_ADD_LOCAL_USER + CONCOURSE_MAIN_TEAM_LOCAL_USER 组合创建本地管理员账号并授权到 main 团队。
  3. worker 必须 privileged: true(containerd 运行时需要完整的内核能力),优雅停止信号用 SIGUSR2。
  4. worker 的 DNS 代理参数在 8.3.1 中以命令行参数方式显式给出最稳妥(实测该两个参数的环境变量写法不生效,详见第十节常见问题)。

六、启动服务栈

cd /opt/concourse && docker compose up -d

实测输出:

 Container concourse-831-stack-db-1 Waiting
 Container concourse-831-stack-db-1 Healthy
 Container concourse-831-stack-web-1 Starting
 Container concourse-831-stack-web-1 Started
 Container concourse-831-stack-worker-1 Starting
 Container concourse-831-stack-worker-1 Started

查看栈状态:

docker compose ps

实测输出:

NAME                           IMAGE                       COMMAND                   SERVICE   CREATED              STATUS                    PORTS
concourse-831-stack-db-1       postgres:16-alpine          "docker-entrypoint.s…"   db        41 minutes ago       Up 41 minutes (healthy)   5432/tcp
concourse-831-stack-web-1      concourse/concourse:8.3.1   "dumb-init /usr/loca…"   web       41 minutes ago       Up 41 minutes             0.0.0.0:8148->8080/tcp, [::]:8148->8080/tcp
concourse-831-stack-worker-1   concourse/concourse:8.3.1   "dumb-init /usr/loca…"   worker    About a minute ago   Up About a minute

web 服务就绪后,atc.listening 与 tsa.listening 日志出现即代表 API 与注册网关均正常:

docker compose logs web --tail 3

实测输出(节选):

web-1  | {"timestamp":"2026-10-20T06:40:42.803214056Z","level":"info","source":"tsa","message":"tsa.listening","data":{}}
web-1  | {"timestamp":"2026-10-20T06:40:42.805293710Z","level":"info","source":"atc","message":"atc.listening","data":{"debug":"127.0.0.1:8079","http":"0.0.0.0:8080"}}

worker 成功注册的关键日志为 worker.beacon-runner.beacon.registered:

worker-1  | {"timestamp":"2026-10-20T06:41:35.406301957Z","level":"info","source":"worker","message":"worker.beacon-runner.beacon.registered","data":{"session":"4.1"}}

七、安装 fly CLI

fly 从 Concourse web 服务直接下载,天然与服务端版本一致:

curl -sL 'http://127.0.0.1:8148/api/v1/cli?arch=amd64&platform=linux' -o /usr/local/bin/fly
chmod +x /usr/local/bin/fly
fly --version

实测输出:

8.3.1

💡 v7.14.0 起也可从浏览器访问 http://192.168.146.128:8148/download-fly 下载对应平台的 fly(实测该页面返回 HTTP 200)。

八、fly 登录与流水线设置

8.1 登录

fly --target main login -c http://127.0.0.1:8148 -u root -p q1w2e3r4

实测输出:

logging in to team 'main'


target saved

登录成功后凭据保存在 ~/.flyrc,后续命令用 -t main 引用该目标。

8.2 编写 hello-world 流水线

创建 pipelines/hello-world.yml:

jobs:
- name: hello-world
  plan:
  - task: say-hello
    config:
      platform: linux
      image_resource:
        type: registry-image
        source:
          repository: docker.1ms.run/library/busybox
          tag: latest
      run:
        path: echo
        args: ["hello world"]

💡 内网环境请把 repository 换成内部制品仓库代理地址;registry-image 资源类型在构建初始化阶段会先做远端版本检查。

8.3 set-pipeline 设置流水线

fly -t main set-pipeline -p hello-world -c pipelines/hello-world.yml

命令回显配置差异并等待确认(输入 y 回车):

实测输出:

jobs:
  job hello-world has been added:
+ name: hello-world
+ plan:
+ - config:
+     image_resource:
+       name: ""
+       source:
+         repository: docker.1ms.run/library/busybox
+         tag: latest
+       type: registry-image
+     platform: linux
+     run:
+       args:
+       - hello world
+       path: echo
+   task: say-hello

pipeline name: hello-world

apply configuration? [yN]: y

确认后配置保存。新流水线默认处于 paused 状态,查看列表:

fly -t main pipelines

实测输出(paused 列为 yes):

2  hello-world  yes  no  2026-10-20 15:13:29 +0800 CST

⚠️ 实测提示:8.3.1 的 set-pipeline 确认交互是原始键盘模式(TUI)。在真实终端里输入 y 即完成;若通过管道/脚本非交互输入(如 echo y | fly ...),客户端可能出现 bailing out 字样的退出信息,这属于非终端环境下的交互怪癖——此时应以 fly pipelines 的实际结果为准判断是否保存成功。纯脚本场景可用 -n 参数配合 API 完成,或保持人工终端操作。

8.4 解除暂停

fly -t main unpause-pipeline -p hello-world

实测输出:

unpaused 'hello-world'

再次查看列表,paused 已变为 no:

2  hello-world  no  no  2026-10-20 15:13:29 +0800 CST

九、触发构建与验证

9.1 触发 hello-world 并实时观察

fly -t main trigger-job -j hello-world/hello-world --watch

实测输出(下载进度条逐帧刷新,此处截取首末帧):

started hello-world/hello-world #6

initializing
initializing check: image
selected worker: worker-01
fetching docker.1ms.run/library/busybox@sha256:fd7dc98638c8e305f4dc34e979f1c0fdfdcaeb0fbf8fcff77ae834b6da3d7e6e
37bb94b0940b [----------------------------------------------] 0.0b/2.1MiB
……
37bb94b0940b [============>-----------------------------] 682.3KiB/2.1MiB
37bb94b0940b [============================================] 2.1MiB/2.1MiB
selected worker: worker-01
running echo hello world
hello world
succeeded

首跑耗时约 13 秒(含远端版本检查与基础环境首次拉取);再次触发时命中 worker 本地资源缓存,8 秒内完成。

9.2 构建历史与作业状态

fly -t main builds -j hello-world/hello-world

实测输出(前 4 行):

6  hello-world/hello-world/6  succeeded  2026-10-20@15:21:23+0800  2026-10-20@15:21:36+0800  13s  main  root
5  hello-world/hello-world/5  errored    2026-10-20@15:20:03+0800  2026-10-20@15:20:03+0800  0s   main  root
4  hello-world/hello-world/4  errored    2026-10-20@15:18:43+0800  2026-10-20@15:18:43+0800  0s   main  root
3  hello-world/hello-world/3  errored    2026-10-20@15:17:43+0800  2026-10-20@15:17:43+0800  0s   main  root
fly -t main jobs -p hello-world

实测输出:

hello-world  no  succeeded  n/a

9.3 服务端信息核验

curl -s http://127.0.0.1:8148/api/v1/info

实测输出:

{"version":"8.3.1","worker_version":"3.0","feature_flags":{"build_rerun":false,"cache_streamed_volumes":false,"global_resources":false,"resource_causality":false},"external_url":"http://192.168.146.128:8148","cluster_name":"quzuo-lab"}

9.4 Web UI 验证

浏览器访问 http://192.168.146.128:8148:

  1. 页面标题为 Concourse,跳转到登录页;
  2. 使用 root / q1w2e3r4 登录 main 团队;
  3. 首页可见 hello-world 流水线拓扑图(一个 job 节点),点击节点进入构建详情;
  4. 构建页可看到 say-hello 任务的实时日志(hello world)与绿色 succeeded 状态。

命令行同样可核验页面可达性:

curl -sI http://127.0.0.1:8148/ | head -1

实测输出:

HTTP/1.1 200 OK

十、常见问题与排查

10.1 worker 反复重启:缺 tsa_host_key.pub

现象:worker 服务状态 Restarting,日志报 error: invalid argument for flag '--tsa-public-key' (expected flag.AuthorizedKeys): failed to read authorized keys: open /concourse-keys/tsa_host_key.pub: no such file or directory。

处理:把 web 侧的 tsa_host_key.pub 拷贝到 worker 密钥目录后重建 worker 服务:

cp /opt/concourse/keys/web/tsa_host_key.pub /opt/concourse/keys/worker/
cd /opt/concourse && docker compose up -d worker

10.2 构建报 DNS 解析失败

现象:构建初始化阶段 image check failed,日志含 lookup docker.1ms.run ... on 10.89.58.4:53: read udp ...: no route to host(或 connection refused)。

原因:8.3.1 的 containerd 运行时给构建隔离环境下发的 DNS 指向 worker 自身(如 10.89.58.4:53),但 DNS 代理进程默认未启用,且 worker 自带的防火墙规则默认拒绝来自构建网络的入站流量。

处理(两步):

  1. worker 以命令行参数显式启用 DNS 代理并指定上游 DNS(实测环境变量 CONCOURSE_WORKER_CONTAINERD_DNS_PROXY_ENABLE / CONCOURSE_WORKER_CONTAINERD_DNS_SERVER 写法在 8.3.1 不生效,命令行参数有效):

    command: worker --containerd-dns-proxy-enable --containerd-dns-server=223.5.5.5
    

    启用后可在 worker 服务内验证 53 端口监听:

    UNCONN 0      0                  *:53               *:*    users:(("concourse",pid=7,fd=8))
    
  2. 若仍报 no route to host,在 worker 服务内放行构建网络到 53 端口的入站:

    docker exec concourse-831-stack-worker-1 iptables -I INPUT 1 -i concourse0 -p udp --dport 53 -j ACCEPT
    docker exec concourse-831-stack-worker-1 iptables -I INPUT 2 -i concourse0 -p tcp --dport 53 -j ACCEPT
    

    (worker 重建后规则需重加,建议写入服务启动脚本。)

10.3 基础环境拉取缓慢或失败

registry-image 资源默认从 Docker Hub 拉取,受限网络下建议:改用内网可达的制品仓库代理地址作为 repository,或预先在离线环境导出/导入软件包。

10.4 栈的启停与销毁

# 停止(保留数据卷)
docker compose -p concourse-831-stack down

# 销毁(含 PostgreSQL 数据卷,流水线与构建历史将清空)
docker compose -p concourse-831-stack down -v

十一、本系列篇目