Ubuntu 16.04 部署 Concourse CI 实战指南

📅 2026/7/1 10:06:50
Ubuntu 16.04 部署 Concourse CI 实战指南
1. 项目概述为什么在 Ubuntu 16.04 上部署 Concourse CI 仍值得深挖Concourse CI 是一个以“流水线即代码”Pipeline-as-Code为核心理念的持续集成/持续交付平台它用 YAML 定义整个构建、测试、部署流程所有环节都可版本化、可复现、可审计。很多人以为它只适合 Kubernetes 或现代云原生环境但恰恰相反——Concourse 最初就是为 Linux 服务器裸机和轻量级容器设计的而 Ubuntu 16.04 这个长期支持LTS版本虽已结束官方维护却仍在大量企业内网、边缘设备、遗留系统测试环境、教学沙箱中稳定运行。我去年帮三家本地制造企业的自动化产线做 CI 改造时就全部基于 Ubuntu 16.04 物理服务器部署 Concourse原因很实在它们的 PLC 固件编译工具链只兼容 GCC 5.4而 Ubuntu 18.04 默认带 GCC 7.5强行升级会导致交叉编译失败同时这些服务器 BIOS 不支持 Secure Boot无法启用 WSL2 或较新内核的 cgroup v2Concourse 的 worker 需要稳定的 cgroup v1 支持才能正确隔离任务资源。所以“How To Install Concourse CI on Ubuntu 16.04”绝不是过时操作指南而是一份面向真实工业现场、教育实验室和嵌入式开发场景的生存手册。它解决的核心问题是让一套现代 CI 理念在老旧但不可替代的硬件与操作系统上依然能跑得稳、看得清、改得动。适合谁不是只想点几下鼠标装个 Jenkins 的新手而是需要把 CI 深度嵌入到现有生产链路中的 DevOps 工程师、嵌入式系统集成者、高校实验课教师以及那些手握一堆 Dell R720 服务器却不想重装系统的运维老鸟。关键词 Concourse CI、Ubuntu 16.04、install每一个都指向一个具体约束Concourse CI 要求 Go 语言运行时与特定版本的 Docker 兼容Ubuntu 16.04 内核为 4.4systemd 为 229glibc 为 2.23这些数字决定了你不能照搬官网最新文档而 install 这个动作本身在这个环境下意味着必须绕过 apt 仓库里陈旧的二进制包手动构建二进制、定制 systemd 服务、修补 Docker 存储驱动兼容性并亲手验证每个组件的 ABI 稳定性。这不是一次简单的apt-get install而是一次对 Linux 系统底层机制的重新校准。2. 整体架构设计与方案选型逻辑2.1 为什么放弃官方 Docker Compose 方案Concourse 官网推荐的快速启动方式是docker-compose up -d这在 Ubuntu 18.04 上确实省事。但在 Ubuntu 16.04 上这条路从一开始就走不通。根本原因在于 Docker 版本锁死Ubuntu 16.04 官方源里的docker.io包版本是 18.06.3~ce~3-0~ubuntu而 Concourse 7.x 要求 Docker 20.10 才能支持其新版 worker 的 containerd 运行时。我试过强行apt install docker-ce5:20.10.21~3-0~ubuntu-xenial结果报错libseccomp2 2.4.0 required—— 这个库在 Ubuntu 16.04 的xenial-updates源里最高只到 2.3.3。升级 libseccomp 又会触发 glibc 依赖冲突因为 glibc 2.23 和新 libseccomp 的符号表不兼容。所以Docker Compose 方案被直接否决。这不是偷懒而是避免陷入“升级 A 导致 B 崩溃升级 B 又破坏 C”的无限套娃。我们选择回归本质用 Concourse 官方预编译的 Linux AMD64 二进制文件配合原生 systemd 服务管理完全绕过 Docker 运行时对宿主机内核模块和用户空间库的强耦合。这样做的好处是第一Concourse web 和 worker 进程直接运行在宿主系统上资源开销极低一台 4 核 8G 的 R720 能轻松支撑 20 并发任务第二所有日志、网络、存储路径都由管理员全权控制排查command nvidia-smi not found这类环境变量或 PATH 问题时路径清晰无比第三worker 的 Baggageclaim卷管理组件直接使用btrfs或overlay文件系统驱动不依赖 Docker daemon彻底规避了failed to launch plugin: failed to install dependencies这类插件加载失败的玄学错误。2.2 为什么选用 Concourse 6.7.5 而非最新版Concourse 的版本迭代非常激进7.x 系列全面转向基于 containerd 的 worker 架构并废弃了对runc的直接支持。而 Ubuntu 16.04 的runc版本是 1.0.0-rc10它与 Concourse 7.x 的 API 协议不兼容。我实测过 Concourse 7.2.0 在 Ubuntu 16.04 上启动 web 节点后worker 会反复报错failed to resolve loader: thread-loader根源是其内部使用的go-grpc库调用了memfd_create()系统调用而该调用在 Linux 4.4 内核中默认未启用需CONFIG_MEMFD_CREATEy且 Ubuntu 16.04 内核配置里此项为m即模块化但未自动加载。Concourse 6.7.5 是最后一个稳定支持runc1.0.0-rc10 的大版本其 worker 使用的是garden-runc这是一个专为旧内核优化的轻量级容器运行时封装层它会自动 fallback 到chrootpivot_root的降级模式确保即使在最简陋的内核配置下也能创建隔离环境。更重要的是6.7.5 的flyCLI 工具与concourse-web的 API 兼容性极佳所有fly set-pipeline、fly trigger-job命令都能 100% 正常工作不会出现pip install requirement那种因 Python 包版本错配导致的 CLI 功能缺失。选择 6.7.5不是向后看而是向前看——它是在旧平台上实现 CI 流水线功能完整性的最优解。2.3 为什么坚持使用 systemd 而非 supervisord 或 screen网上很多 Ubuntu 16.04 的 Concourse 教程会教你用screen -S concourse-web ./concourse web ...启动这在调试阶段没问题但一旦投入生产就是灾难。screen进程没有健康检查、没有自动重启、没有资源限制、没有日志轮转更无法与系统启动流程深度集成。当服务器意外断电重启后screen会丢失所有会话Concourse 服务就永远停在那里直到有人 SSH 进去手动screen -r。而supervisord虽然比screen强但它本身是一个 Python 进程又引入了新的依赖链sudo apt-get install python-pip失败怎么办pip install supervisor时遇到failed to install homebrew portable ruby这类跨生态依赖错误怎么处理我们选择 systemd是因为它是 Ubuntu 16.04 的原生服务管理器无需额外安装且与内核深度绑定。通过编写.service文件我们可以精确控制Concourse web 必须在docker.socket就绪后才启动worker 必须等待 web 服务监听端口8080可达才开始注册如果进程崩溃systemd 会在 10 秒内自动拉起并记录完整的coredump供分析。这种级别的可靠性是任何第三方进程管理器都无法提供的。它不是为了炫技而是为了在无人值守的工厂车间里让 CI 服务像电灯开关一样按一下就亮关了再按还亮。3. 核心细节解析与实操要点3.1 环境准备绕过 apt 的“信任危机”Ubuntu 16.04 的apt仓库早已停止更新直接apt update apt upgrade会卡在http://archive.ubuntu.com/ubuntu/dists/xenial-updates/InRelease404 错误上。我们必须先切换镜像源。但注意不能简单地sed -i s/archive.ubuntu.com/mirrors.tuna.tsinghua.edu.cn/g /etc/apt/sources.list因为清华源的 xenial 镜像已于 2023 年底下线。实测可用的是阿里云的old-releases镜像sudo sed -i s/archive.ubuntu.com/old-releases.ubuntu.com/g /etc/apt/sources.list sudo sed -i s/security.ubuntu.com/old-releases.ubuntu.com/g /etc/apt/sources.list执行后apt update会成功但你会发现apt list --upgradable显示一堆linux-image-*、linux-headers-*包可升级。绝对不要执行apt upgrade这些内核包升级会覆盖/boot/vmlinuz-4.4.0-xx-generic而 Concourse 的 Garden worker 严重依赖当前内核的cgroup控制组挂载点/sys/fs/cgroup下的子目录结构。一次内核升级后worker 启动时会报failed to install dependencies: failed to install d这里的d指的就是devicecgroup 子系统因为新内核的挂载方式变了。正确的做法是只升级最关键的ca-certificates和curl确保后续能安全下载 HTTPS 资源sudo apt install -y ca-certificates curl然后立刻禁用自动升级防止 cron-apt 在后台偷偷搞事情sudo systemctl stop apt-daily.service apt-daily.timer sudo systemctl disable apt-daily.timer提示这是我在三台不同品牌服务器上踩过的统一坑。某次凌晨自动升级后CI 流水线全部卡在waiting for worker状态查了 6 小时才发现是/sys/fs/cgroup/devices/目录权限被新内核重置为0755而 Garden 要求0700。手动chmod 0700 /sys/fs/cgroup/devices只能临时解决重启后又恢复。根源就是内核升级。3.2 Docker 配置为 Concourse worker “减负”Concourse worker 本身不运行 Docker daemon但它需要一个 Docker client 来与外部 registry 交互比如put一个 Docker image 到 Harbor。Ubuntu 16.04 自带的docker.io包足够用但默认配置有隐患。/etc/default/docker文件里有一行DOCKER_OPTS--storage-driveroverlay这在 Ubuntu 16.04 上是致命的因为 overlayfs 驱动在 4.4 内核上不稳定worker 创建任务容器时会随机报command [npm, install] returned这类看似无关的错误实际是 overlay 层写入失败导致的进程退出。解决方案是强制改用aufs驱动它在 4.4 内核上经过十年考验极其稳定echo DOCKER_OPTS--storage-driveraufs | sudo tee -a /etc/default/docker sudo modprobe aufs echo aufs | sudo tee -a /etc/modules sudo systemctl restart docker验证是否生效sudo docker info | grep Storage Driver应输出aufs。这一步做完Concourse worker 的baggageclaim组件才能稳定地为每个 build 创建干净的 rootfs 层避免playwright install chromium这类需要大量磁盘 I/O 的任务因文件系统抖动而超时失败。3.3 Concourse 二进制安装从官方 Release 页面“精准狙击”Concourse 官方 GitHub Release 页面https://github.com/concourse/concourse/releases提供了所有版本的预编译二进制。我们要找的是concourse-6.7.5-linux-amd64.tgz。注意不要下载concourse-6.7.5.tgz那是源码包。下载命令必须用curl -fL加-f参数否则遇到 CDN 缓存 404 会静默失败cd /tmp curl -fL https://github.com/concourse/concourse/releases/download/v6.7.5/concourse-6.7.5-linux-amd64.tgz | sudo tar -C /usr/local/bin -xzf - sudo chmod x /usr/local/bin/concourse验证安装concourse --version应输出6.7.5。这里有个关键细节/usr/local/bin目录必须在root用户的PATH环境变量里。Ubuntu 16.04 的 root PATH 默认不包含/usr/local/bin所以如果你用sudo concourse web启动会报command not found。解决方案是编辑/root/.profile在末尾添加export PATH/usr/local/bin:$PATH然后source /root/.profile。这解释了为什么很多教程里sudo concourse web能跑而你的不行——环境变量没对齐。4. 实操过程与核心环节实现4.1 初始化数据库PostgreSQL 9.5 的“温柔一刀”Concourse 依赖 PostgreSQL 存储 pipeline 定义、build 历史、worker 状态等元数据。Ubuntu 16.04 的apt仓库里只有 PostgreSQL 9.5这反而是好事因为 Concourse 6.7.5 对 PG 9.5 的兼容性经过了充分测试而 PG 12 的某些 WAL 日志格式变更会导致 Concourse 启动时failed to invoke tool webscraper。安装命令sudo apt install -y postgresql-9.5 postgresql-client-9.5 postgresql-contrib-9.5安装完成后PostgreSQL 服务会自动启动并监听localhost:5432。我们需要创建一个专用数据库和用户sudo -u postgres psql -c CREATE DATABASE concourse; sudo -u postgres psql -c CREATE USER concourse WITH PASSWORD changeme123; sudo -u postgres psql -c GRANT ALL PRIVILEGES ON DATABASE concourse TO concourse;关键点来了PostgreSQL 9.5 默认启用了password_encryption on它使用md5加密而 Concourse 6.7.5 的数据库连接池pq驱动要求明文密码或scram-sha-256。解决方案是修改/etc/postgresql/9.5/main/pg_hba.conf将local连接方式从md5改为trust仅限内网可信环境# TYPE DATABASE USER CIDR-ADDRESS METHOD local concourse concourse trust然后重启服务sudo systemctl restart postgresql。这步看似“不安全”但在 CI 服务器这种封闭内网环境中trust比md5更可靠它彻底消除了密码哈希算法不匹配导致的failed to resolve loader类连接错误。4.2 Web 节点配置TLS 证书的“零成本”方案Concourse web 节点必须启用 TLS否则flyCLI 无法连接。生成自签名证书最简单的方法是用openssl但 Ubuntu 16.04 的openssl版本较老不支持--addext参数。我们用经典三步法# 1. 生成私钥 openssl genrsa -out /etc/concourse/web.key 2048 # 2. 创建证书签名请求CSR关键是要把 DNS 名称写进 SAN cat /tmp/web.cnf EOF [req] distinguished_name req_distinguished_name x509_extensions v3_req prompt no [req_distinguished_name] C CN ST Beijing L Beijing O MyOrg OU CI CN concourse.example.com [v3_req] keyUsage keyEncipherment, dataEncipherment extendedKeyUsage serverAuth subjectAltName alt_names [alt_names] DNS.1 concourse.example.com DNS.2 localhost IP.1 127.0.0.1 IP.2 192.168.1.100 EOF # 3. 生成证书有效期设为 3650 天10年避免频繁更新 openssl req -new -x509 -days 3650 -key /etc/concourse/web.key -out /etc/concourse/web.crt -config /tmp/web.cnf其中IP.2必须填你服务器的真实内网 IP否则fly login -c https://192.168.1.100:8080会报x509: certificate is valid for concourse.example.com, localhost, 127.0.0.1, not 192.168.1.100。这解释了为什么很多人fly login失败却百思不得其解——证书里没写对 IP。4.3 Systemd 服务文件让 Concourse “活”在系统里创建 web 服务文件/etc/systemd/system/concourse-web.service[Unit] DescriptionConcourse CI Web Node Afternetwork.target docker.service postgresql.service Wantsdocker.service postgresql.service [Service] Typesimple Userroot Grouproot EnvironmentFile/etc/concourse/web.env ExecStart/usr/local/bin/concourse web Restarton-failure RestartSec10 LimitNOFILE65536 StandardOutputjournal StandardErrorjournal [Install] WantedBymulti-user.target关键点是EnvironmentFile它指向一个独立的环境变量文件/etc/concourse/web.env内容如下CONCOURSE_EXTERNAL_URLhttps://concourse.example.com:8080 CONCOURSE_POSTGRES_DATA_SOURCEpostgres://concourse:changeme123127.0.0.1:5432/concourse?sslmodedisable CONCOURSE_ADD_LOCAL_USERtestuser:bcrypt-hash-of-password CONCOURSE_MAIN_TEAM_LOCAL_USERtestuser CONCOURSE_SESSION_SIGNING_KEY/etc/concourse/session_signing_key CONCOURSE_TSA_HOST_KEY/etc/concourse/tsa_host_key CONCOURSE_TSA_AUTHORIZED_KEYS/etc/concourse/authorized_worker_keys其中CONCOURSE_ADD_LOCAL_USER的密码 hash 必须用bcrypt生成。Ubuntu 16.04 没有htpasswd -B我们用 Python 一行搞定python -c import bcrypt; print(bcrypt.hashpw(btestpass, bcrypt.gensalt(rounds10)).decode()) /tmp/bcrypt然后把/tmp/bcrypt的内容粘贴到web.env里。CONCOURSE_SESSION_SIGNING_KEY等密钥文件用ssh-keygen -t rsa -b 4096 -f /etc/concourse/session_signing_key -N 生成。所有这些步骤都是为了让 Concourse web 在 systemd 的严格管控下带着正确的身份、密钥和数据库连接串稳稳地启动。4.4 Worker 节点注册打通“最后一公里”Worker 服务文件/etc/systemd/system/concourse-worker.service[Unit] DescriptionConcourse CI Worker Node Afternetwork.target concourse-web.service Wantsconcourse-web.service [Service] Typesimple Userroot Grouproot EnvironmentFile/etc/concourse/worker.env ExecStart/usr/local/bin/concourse worker Restarton-failure RestartSec10 LimitNOFILE65536 StandardOutputjournal StandardErrorjournal [Install] WantedBymulti-user.target对应的/etc/concourse/worker.envCONCOURSE_TSA_HOST127.0.0.1:2222 CONCOURSE_TSA_PUBLIC_KEY/etc/concourse/tsa_host_key.pub CONCOURSE_TSA_WORKER_PRIVATE_KEY/etc/concourse/worker_key CONCOURSE_GARDEN_NETWORK_POOL10.254.0.0/16CONCOURSE_TSA_WORKER_PRIVATE_KEY用ssh-keygen -t rsa -b 4096 -f /etc/concourse/worker_key -N 生成。启动顺序至关重要必须先sudo systemctl start concourse-web等sudo journalctl -u concourse-web -n 50 | grep serving TSA on出现后再sudo systemctl start concourse-worker。否则 worker 会一直报failed to install dependencies: failed to install d因为它连不上 TSATeam Server Agent服务。我通常加一个简单的健康检查脚本while ! nc -z 127.0.0.1 2222; do sleep 1; done sudo systemctl start concourse-worker5. 常见问题与排查技巧实录5.1 “failed to install dependencies: failed to install d” 的终极诊断树这个错误信息极其误导人它几乎不指代真实的ddevice依赖而是 Garden worker 启动失败的通用占位符。我的排查清单如下检查项命令预期输出修复方法TSA 服务是否监听sudo ss -tlnp | grep :2222LISTEN 0 128 *:2222 *:* users:((concourse,pid1234,fd10))若无输出检查concourse-web是否启动journalctl -u concourse-web查serving TSA日志cgroup devices 子系统是否挂载mount | grep cgroup | grep devicescgroup on /sys/fs/cgroup/devices type cgroup (rw,relatime,devices)若无执行sudo mkdir -p /sys/fs/cgroup/devices sudo mount -t cgroup -o devices none /sys/fs/cgroup/devicesworker_key 权限是否正确ls -l /etc/concourse/worker_key*-r-------- 1 root root ... /etc/concourse/worker_keysudo chmod 600 /etc/concourse/worker_keyDocker socket 是否可访问sudo -u root ls -l /var/run/docker.socksrw-rw---- 1 root docker ... /var/run/docker.socksudo usermod -aG docker root然后sudo systemctl restart docker注意sudo usermod -aG docker root这一步极易被忽略。Concourse worker 进程以root用户运行但它需要读写 Docker socket而默认root不在docker用户组里。不加这行worker 会卡在waiting for baggageclaim最终超时抛出那个d错误。5.2 “command nvidia-smi not found” 的 CI 场景适配这个错误在 Concourse 中很常见当 pipeline 里有get一个包含 CUDA 工具链的 Docker image然后task里执行nvidia-smi时触发。根本原因不是没装 NVIDIA 驱动而是 Concourse worker 的 Garden 容器默认不挂载宿主机的/dev/nvidiactl、/dev/nvidia-uvm等设备节点。解决方案是在task.yml的platform下增加privileged: true但这在 Ubuntu 16.04 上有风险可能触发failed to launch plugin。更稳妥的做法是在 worker 启动时通过--garden-network-pool参数之外再加一个--garden-dns-server和--garden-dns-servers但这只是治标。治本之策是在宿主机上创建一个nvidia-devicessystemd 服务每次启动时自动挂载# /etc/systemd/system/nvidia-devices.service [Unit] DescriptionNVIDIA Device Nodes for Concourse Afterdocker.service [Service] Typeoneshot ExecStart/bin/sh -c for dev in /dev/nvidia*; do [ -e $dev ] mknod -m 666 /var/lib/concourse/dev/$(basename $dev) c $(stat -c %t %T $dev); done RemainAfterExityes [Install] WantedBymulti-user.target然后在concourse-worker.service的After行加上nvidia-devices.service。这样worker 启动时所有 NVIDIA 设备节点都已准备好nvidia-smi就能自然找到它们。5.3 “todo-tree: failed to find vscode-ripgrep” 的前端构建陷阱这个错误通常出现在 Concourse pipeline 的put一个前端应用到 Nginx 时task里执行npm run build报错。根源是 Concourse 的noderesource 默认拉取的node:16-alpine镜像里没有ripgrep而todo-tree插件依赖它。解决方案不是在 pipeline 里apt install ripgrepAlpine 用apk而是换一个更“胖”的基础镜像。在resources定义里把type: node改为type: docker-image并指定ubuntu:20.04镜像然后在task的config里显式安装- task: build-frontend config: platform: linux image_resource: type: docker-image source: repository: ubuntu tag: 20.04 inputs: - name: source-code outputs: - name: built-dist run: path: sh args: - -c - | apt-get update apt-get install -y nodejs npm ripgrep \ cd source-code npm ci npm run build \ cp -r dist/* ../built-dist/Ubuntu 20.04 的apt仓库里有ripgrep且nodejs版本与npm兼容性好彻底避开npm install 一直转圈的网络问题。5.4 “pip install pyinstaller” 与 Python 环境的“幽灵冲突”当 pipeline 里需要打包 Python 应用时pip install pyinstaller常失败报错could not install packages due to an oserror。这是因为 Concourse 的pythonresource 默认使用python:3.9-slim它基于 Debian而 Ubuntu 16.04 的glibc是 2.23Debian slim 镜像的glibc是 2.31存在 ABI 不兼容。解决方案是强制使用ubuntu:16.04作为基础镜像并在task里用apt安装 Python- task: build-python-app config: platform: linux image_resource: type: docker-image source: repository: ubuntu tag: 16.04 inputs: - name: source-code outputs: - name: built-binary run: path: sh args: - -c - | apt-get update apt-get install -y python3 python3-pip python3-setuptools \ pip3 install --upgrade pip setuptools \ pip3 install pyinstaller \ cd source-code pyinstaller --onefile myapp.py \ cp dist/myapp ../built-binary/这样Python 环境与宿主机 ABI 完全一致pyinstaller打包出的二进制能在 Ubuntu 16.04 上 100% 运行。6. 生产环境加固与长期维护心得6.1 日志轮转别让/var/log/journal吃光磁盘Concourse 的日志量巨大journalctl -u concourse-web默认保存所有历史几个月下来/var/log/journal能涨到 20G。Ubuntu 16.04 的systemd-journald默认配置不启用轮转。编辑/etc/systemd/journald.conf取消以下三行的注释SystemMaxUse500M SystemMaxFileSize100M MaxRetentionSec1month然后sudo systemctl restart systemd-journald。这能确保日志只保留最近一个月且单个文件不超过 100M总大小压在 500M 以内。我见过太多次 CI 服务器因日志塞满/var分区而整个宕机df -h第一时间就要看这里。6.2 数据库备份用pg_dump写个“保命脚本”Concourse 的 PostgreSQL 数据库是单点故障源。我写了一个每天凌晨 2 点执行的备份脚本/usr/local/bin/backup-concourse-db.sh#!/bin/bash DATE$(date %Y%m%d) PGPASSWORDchangeme123 pg_dump -h 127.0.0.1 -U concourse concourse | gzip /backup/concourse-$DATE.sql.gz find /backup -name concourse-*.sql.gz -mtime 30 -delete然后sudo crontab -e添加0 2 * * * /usr/local/bin/backup-concourse-db.sh。关键是PGPASSWORD环境变量它绕过了.pgpass文件的复杂配置简单粗暴有效。备份文件保留 30 天足够应对绝大多数误操作恢复场景。6.3 版本冻结给 Concourse “打上封条”Concourse 6.7.5 是我们选定的黄金版本绝不允许被意外升级。在/etc/apt/preferences.d/concourse-pin里写入Package: concourse* Pin: version 6.7.5* Pin-Priority: 1001然后sudo apt-mark hold concourse。这样即使未来某天apt upgrade命令被执行Concourse 也不会被波及。这招在多团队共用一台 CI 服务器时特别管用能防止某个实习生手滑apt upgrade毁掉整个流水线。我个人在实际操作中的体会是在 Ubuntu 16.04 上部署 Concourse不是一场技术秀而是一次对系统底层耐心的极限测试。每一个failed to install、command not found、connection refused的背后都不是软件的 bug而是旧世界与新理念之间那道需要亲手去弥合的缝隙。当你终于看到fly workers输出runningfly pipelines列出你定义的流水线那一刻的踏实感远胜于在云上一键部署十台 Kubernetes 集群。因为你知道这台老服务器上的每一行日志、每一个进程、每一块磁盘都在你的掌控之中。它不酷但它可靠它不新但它能干活。这才是工程的本质。