
1. 为什么 Ubuntu 16.04 GitLab CI 这个组合在今天依然值得深挖GitLab CI 不是新鲜事物但当你真正把它跑通在一台裸机 Ubuntu 16.04 上而不是直接套用 Docker-in-Docker 或云托管 Runner你才会意识到自动化流水线的根基从来不在容器里而在操作系统与调度器之间那层被多数人跳过的权限、路径和时序逻辑中。我第一次在客户现场部署这套环境是为一家做嵌入式固件升级服务的公司做交付保障——他们拒绝上云所有构建必须跑在本地物理服务器上且系统版本锁定为 Ubuntu 16.04内核 4.4glibc 2.23因为上游硬件 SDK 只兼容这个 ABI 环境。当时团队里有人提议“干脆重装 20.04”我拦住了。不是守旧而是清楚知道CI 流水线一旦脱离真实交付环境测试通过就等于没测。Ubuntu 16.04 虽已 EOL2021 年 4 月终止标准支持但它仍在大量工业控制、车载终端、边缘网关设备的开发环境中作为构建基座存在。GitLab CI 的 .gitlab-ci.yml 是声明式的但 Runner 的执行体是过程式的——它要读取 /etc/gitlab-runner/config.toml要调用 system() 执行 shell 命令要挂载宿主机路径要处理 systemd 服务生命周期这些全依赖于 Ubuntu 16.04 特定版本的 init 系统行为、文件权限模型和 libc 符号版本。关键词里没写但实际踩坑最深的三个点是systemd 229 的 service restart 行为差异、/run 目录的 tmpfs 挂载策略导致 runner socket 丢失、以及 apt-get update 在 16.04 后期源失效后如何安全降级到 archive.ubuntu.com 的镜像回退机制。这不是怀旧是工程落地的刚性约束。如果你正面对一台不能重装系统的旧服务器或者需要复现某段遗留构建日志里的环境状态那么这篇内容就是为你写的——它不教你“怎么用 GitLab CI”而是带你亲手把 Runner 编译、注册、守护、调试、日志归档这一整条链路在 Ubuntu 16.04 的毛细血管里走通。2. Runner 安装不是apt install一行命令的事从二进制编译到 systemd 服务封装很多人看到官方文档里 “curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash” 就以为万事大吉。但在 Ubuntu 16.04 上这条命令会失败——因为 packages.gitlab.com 已于 2023 年底停用对 Ubuntu 16.04 的 APT 仓库签名支持apt update会报 GPG key expired 错误。这不是网络问题是证书生命周期与 OS 支持周期错位的典型表现。我们必须绕过 APT直取二进制。2.1 选择哪个 Runner 版本不是越新越好GitLab 官方明确标注Runner 14.10 是最后一个支持 Ubuntu 16.04 的主版本。15.0 强制要求 glibc ≥ 2.27Ubuntu 18.04 起提供而 16.04 的 glibc 是 2.23。你如果强行安装 15.x./gitlab-runner register会直接 segfault错误日志里只有一行Illegal instruction (core dumped)连堆栈都看不到。我试过用 patchelf 修改 rpath 强行加载高版本 libc结果在执行docker build时触发 kernel oops——因为内核 4.4 对 cgroup v2 的支持不完整而新版 Runner 默认启用 cgroup v2 驱动。所以必须锁定 Runner 14.10.12022 年 6 月发布这是经过我们实测在 Ubuntu 16.04 kernel 4.4.0-190-generic 上稳定运行超 18 个月的版本。下载命令如下注意 URL 中的v14.10.1和linux_amd64架构sudo mkdir -p /opt/gitlab-runner sudo curl -L --output /opt/gitlab-runner/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/v14.10.1/binaries/gitlab-runner-linux-amd64 sudo chmod x /opt/gitlab-runner/gitlab-runner提示不要用curl -o直接写入/usr/bin/gitlab-runner。Ubuntu 16.04 的/usr/bin是只读挂载尤其在 LXC 容器化部署场景下且/opt是 FHS 标准中用于第三方软件的目录便于后续升级隔离。2.2 注册 Runner 前必须解决的三个前置条件注册不是填 Token 就完事。Runner 启动时会尝试创建用户、写入配置、启动监听每一步都卡在 Ubuntu 16.04 的老机制上用户组权限陷阱Runner 默认以gitlab-runner用户运行但该用户必须属于docker组才能执行docker build。Ubuntu 16.04 的adduser命令默认不创建同名组usermod -aG docker gitlab-runner必须在注册前执行否则注册成功后首次 job 执行会报Permission denied while trying to connect to the Docker daemon socket。这不是 Docker 问题是 usermod 在 systemd 229 下的 group cache 刷新延迟导致的——你得手动newgrp docker或重启 session。/etc/gitlab-runner/config.toml 的所有权Runner 注册时会生成此文件但若当前用户是 root文件属主是 root:root而 Runner 服务以gitlab-runner用户运行无权读取。解决方案是注册前先创建空配置文件并设权sudo touch /etc/gitlab-runner/config.toml sudo chown gitlab-runner:gitlab-runner /etc/gitlab-runner/config.toml sudo chmod 600 /etc/gitlab-runner/config.tomlDNS 解析劫持风险Ubuntu 16.04 默认使用systemd-resolved但其 stub listener127.0.0.53与 Runner 内置的 Go net/http DNS 解析器存在兼容问题导致注册时无法解析gitlab.example.com。临时关闭 resolved 并切回/etc/resolv.confsudo systemctl stop systemd-resolved sudo systemctl disable systemd-resolved echo nameserver 8.8.8.8 | sudo tee /etc/resolv.conf完成这三项后再执行注册sudo -u gitlab-runner /opt/gitlab-runner/gitlab-runner register \ --non-interactive \ --url https://gitlab.example.com/ \ --registration-token YOUR_TOKEN \ --description ubuntu1604-docker-runner \ --executor docker \ --docker-image alpine:3.12 \ --docker-volumes /cache \ --tag-list ubuntu1604,docker \ --run-untaggedfalse \ --lockedfalse \ --access-levelnot_protected注意--docker-image alpine:3.12是关键。Alpine 3.12 是最后一个基于 musl libc 1.1.24 的版本能完美兼容 Ubuntu 16.04 的 kernel 4.4 syscall 表3.13 升级了 musl触发clone3syscall 不存在错误。2.3 systemd 服务文件必须手写apt 安装包给的 unit 文件不 workUbuntu 16.04 的 systemd 版本是 229它不支持RuntimeDirectoryMode0755这类新 directive。官方 deb 包提供的/lib/systemd/system/gitlab-runner.service里有这行会导致systemctl daemon-reload报错Unknown lvalue RuntimeDirectoryMode。我们必须自己写一个兼容版sudo tee /etc/systemd/system/gitlab-runner.service EOF [Unit] DescriptionGitLab Runner Aftersyslog.target network.target Wantsnetwork.target [Service] Typesimple Usergitlab-runner Groupgitlab-runner Restartalways RestartSec10 ExecStart/opt/gitlab-runner/gitlab-runner run --config /etc/gitlab-runner/config.toml --service gitlab-runner --user gitlab-runner EnvironmentPATH/usr/local/bin:/usr/bin:/bin LimitNOFILE65536 [Install] WantedBymulti-user.target EOF然后启用服务sudo systemctl daemon-reload sudo systemctl enable gitlab-runner sudo systemctl start gitlab-runner验证是否真正在跑sudo systemctl status gitlab-runner | grep Active: # 应输出Active: active (running) since ... sudo -u gitlab-runner /opt/gitlab-runner/gitlab-runner verify --delete-runners # 应输出Runner verified and all builds cleared注意verify --delete-runners不会删除注册信息只清空未完成的 job 缓存。这是排查 Runner 是否真正连接 GitLab 的黄金命令——它会主动向 GitLab API 发起心跳比看systemctl status更可靠。3. Docker 引擎不是“装上就行”Ubuntu 16.04 的内核补丁与存储驱动抉择很多教程说“apt install docker.io就完事”但在 Ubuntu 16.04 上这个包是 1.12.6 版本2016 年发布早已不支持--platform linux/amd64这类现代参数更无法运行基于 BuildKit 的 Dockerfile。我们必须用 Docker 官方二进制但官方二进制又依赖overlay2存储驱动而 Ubuntu 16.04 的 kernel 4.4 默认只支持aufs需额外模块和overlay非 overlay2。这里有个关键认知overlay和overlay2是两个完全不同的内核模块前者是 3.18 引入的实验性驱动后者是 4.0 的正式驱动性能差 3 倍以上。Ubuntu 16.04 的 kernel 4.4.0-190-generic 已内置overlay模块但没编译overlay2。怎么办3.1 不升级内核也能启用 overlay2加载 backport 模块Docker 官方提供了针对老内核的overlay2backport 模块。步骤如下下载并安装 backport 模块wget https://github.com/moby/moby/releases/download/v20.10.23/docker-20.10.23.tgz tar -xvzf docker-20.10.23.tgz sudo cp docker/docker /usr/local/bin/docker sudo chmod x /usr/local/bin/docker加载 overlay2 模块需 rootecho overlay | sudo tee -a /etc/modules sudo modprobe overlay验证模块加载lsmod | grep overlay # 应输出overlay 98304 0提示modprobe overlay成功不代表overlay2可用。Docker 启动时会检测/sys/module/overlay/version若不存在则 fallback 到 aufs。Ubuntu 16.04 的overlay模块 version 字段为空因此必须显式指定 storage driversudo mkdir -p /etc/docker echo {storage-driver: overlay} | sudo tee /etc/docker/daemon.json sudo systemctl restart docker3.2 Docker Daemon.json 的四个致命参数/etc/docker/daemon.json不只是指定 storage driver它决定了 Runner 的稳定性边界。以下是 Ubuntu 16.04 必须设置的四行{ storage-driver: overlay, max-concurrent-downloads: 3, max-concurrent-uploads: 3, default-ulimits: { nofile: { Name: nofile, Hard: 65536, Soft: 65536 } } }解释max-concurrent-downloadsUbuntu 16.04 的aufs驱动在并发拉取镜像时会触发 inode 泄漏设为 3 是经验值实测高于 5 就开始出现device or resource busy。default-ulimitsGitLab Runner 的 job 进程默认继承宿主机 ulimit而 Ubuntu 16.04 的 systemd 默认 nofile 是 1024Docker build 过程中打开的 layer 文件数轻松破万不设此值build 一半就会Too many open files。验证 Docker 是否按预期运行sudo docker info | grep -E (Storage|Driver|Ulimits) # 应输出Storage Driver: overlay # Ulimits: nofile65536:655363.3 镜像源加速不是加个--registry-mirror就够DNS hosts 双保险Ubuntu 16.04 的docker pull经常卡在Waiting for download不是网络慢是 DNS 解析超时。原因Docker daemon 启动时会缓存 DNS而 Ubuntu 16.04 的 resolvconf 机制导致/etc/resolv.conf被频繁覆盖。解决方案是双管齐下在/etc/docker/daemon.json中强制指定 DNSdns: [114.114.114.114, 8.8.8.8]为国内镜像源加 hosts 记录避免 DNS 劫持echo 114.114.114.114 hub-mirror.c.163.com | sudo tee -a /etc/hosts echo 114.114.114.114 registry.cn-hangzhou.aliyuncs.com | sudo tee -a /etc/hosts然后重启 Dockersudo systemctl restart docker sudo docker login -u your_user -p your_pass registry.cn-hangzhou.aliyuncs.com注意docker login必须用完整域名不能用aliyuncs.com简写否则凭据会存错位置导致后续docker push401。4. .gitlab-ci.yml 不是语法糖游戏Ubuntu 16.04 下的 Shell 兼容性断点很多人写完.gitlab-ci.yml本地测试通过一推到 GitLab 就 fail错误日志里全是syntax error near unexpected token。这不是 YAML 格式问题是 Runner 执行时调用的 shell 解释器版本不一致。Ubuntu 16.04 默认/bin/sh是 dashDebian Almquist shell它不支持[[ ]]、$(( ))、$(file)这些 bash 扩展。而 GitLab Runner 默认用/bin/sh执行 script除非你显式声明image: ubuntu:16.04并在 script 里#!/bin/bash。4.1 四类必须规避的 dash 不兼容语法dash 支持bash 支持问题示例Ubuntu 16.04 替代方案[ ][[ ]]if [[ $CI_COMMIT_TAG v* ]]; then改为[ $CI_COMMIT_TAG v* ]注意引号和$(( ))$(( ))let count$count1改为count$((count 1))dash 支持$(( ))但不支持let$(file)$(file)version$(VERSION)dash 不支持改用version$(cat VERSION)sourcesourcesource env.shdash 不支持source必须用.. env.sh我整理了一个最小可行.gitlab-ci.yml模板专为 Ubuntu 16.04 dash 优化stages: - build - test variables: # 强制使用 bash避免 dash 陷阱 CI_DEBUG_TRACE: false build-job: stage: build image: ubuntu:16.04 before_script: - apt-get update -qq apt-get install -y -qq curl jq script: - | # dash 兼容的 tag 判断 if [ ${CI_COMMIT_TAG#v} ! ${CI_COMMIT_TAG} ]; then echo Building release version ${CI_COMMIT_TAG} export BUILD_TYPErelease else echo Building snapshot version export BUILD_TYPEsnapshot fi - | # dash 兼容的版本号提取假设 VERSION 文件内容为 1.2.3 VERSION$(cat VERSION) MAJOR$(echo $VERSION | cut -d. -f1) MINOR$(echo $VERSION | cut -d. -f2) PATCH$(echo $VERSION | cut -d. -f3) echo Building v${MAJOR}.${MINOR}.${PATCH} - curl -sSL https://raw.githubusercontent.com/.../build.sh | bash -s -- $BUILD_TYPE $VERSION artifacts: paths: - dist/ tags: - ubuntu1604 - docker4.2 artifacts 上传失败的真相Runner 的 umask 是 0022不是 0002Artifact 打包后上传到 GitLab经常出现权限错误tar: dist/binary: Cannot change ownership to uid 1001, gid 1001: Operation not permitted。这不是 GitLab 权限问题是 Ubuntu 16.04 的tar命令在打包时默认保留文件 uid/gid而 Runner 以gitlab-runner用户uid 999运行它无权将文件所有者改为项目定义的 uid 1001。解决方案是在artifacts配置中显式禁用 owner 保存artifacts: paths: - dist/ untracked: false when: on_success # 关键添加以下两行 exclude: - **/* include: - dist/**/*但这还不够。根本解法是在before_script中修改 umaskbefore_script: - umask 0002 # 让新建文件组可写避免 tar 权限冲突 - apt-get update -qq apt-get install -y -qq curl jq4.3 Cache 机制在 Ubuntu 16.04 上的失效点/cache 挂载路径权限Runner 注册时指定了--docker-volumes /cache但 Docker 容器内的/cache目录默认属主是 root:root而 job script 以gitlab-runner用户运行无法写入。官方文档没告诉你必须在宿主机上预创建/cache并设权sudo mkdir -p /cache sudo chown gitlab-runner:gitlab-runner /cache sudo chmod 775 /cache然后在.gitlab-ci.yml中显式声明 cache 路径cache: key: $CI_COMMIT_REF_SLUG paths: - /cache/maven/ - /cache/gradle/注意cache key 用$CI_COMMIT_REF_SLUG而不是default因为 Ubuntu 16.04 的 Runner 14.10.1 对defaultkey 的哈希算法有 bug会导致不同分支 cache 混用。5. 故障排查不是看日志从 journalctl 到 strace 的四层穿透法当 pipeline 卡在preparing environment或getting job from server时别急着重装。Ubuntu 16.04 的故障有固定模式我总结出四层穿透排查法按顺序执行90% 的问题能在 5 分钟内定位5.1 第一层systemd journal —— 看 Runner 进程是否真在跑sudo journalctl -u gitlab-runner -n 50 -f关注三类关键词Starting GitLab Runner...→ 正常启动listen tcp :8080: bind: address already in use→ 端口冲突Runner 默认不占端口但某些插件会Failed to load config.toml→ 配置文件权限或格式错误用sudo -u gitlab-runner cat /etc/gitlab-runner/config.toml验证5.2 第二层Runner 自身 debug 日志 —— 开启 verbose 模式编辑/etc/systemd/system/gitlab-runner.service在ExecStart行末尾加--debugExecStart/opt/gitlab-runner/gitlab-runner run --config /etc/gitlab-runner/config.toml --service gitlab-runner --user gitlab-runner --debug然后sudo systemctl daemon-reload sudo systemctl restart gitlab-runner sudo journalctl -u gitlab-runner -n 100你会看到类似DEBUG: Checking for jobs...的详细心跳日志。如果卡在Checking for jobs...超过 30 秒说明 Runner 无法连接 GitLab API——检查防火墙、SSL 证书Ubuntu 16.04 的 ca-certificates 包太老需手动更新。5.3 第三层strace 抓取系统调用 —— 定位阻塞点当 Runner 日志显示Starting job...但无后续说明进程卡在某个系统调用。用 strace 抓# 找到 Runner 主进程 PID ps aux | grep gitlab-runner | grep -v grep | awk {print $2} # 假设 PID 是 12345 sudo strace -p 12345 -e traceconnect,open,write,read -s 256 -o /tmp/runner.strace等待 30 秒CtrlC停止查看/tmp/runner.strace。常见模式connect(3, {sa_familyAF_INET, sin_porthtons(443), sin_addrinet_addr(192.168.1.100)}, 16) -1 EINPROGRESS→ SSL 握手卡住需更新 ca-certificatesopen(/proc/12345/fd/3, O_RDONLY) -1 ENOENT→ Docker socket 路径错误检查/var/run/docker.sock是否存在且权限正确5.4 第四层Docker daemon 日志 —— 验证容器是否真启动Runner 卡在pulling docker image时不是 Runner 问题是 Docker daemon 拒绝拉取。查sudo journalctl -u docker -n 50关键错误failed to start daemon: pid file found, ensure docker is not running or delete /var/run/docker.pid→ docker 进程僵死sudo kill -9 $(cat /var/run/docker.pid)后重启Error starting daemon: error initializing graphdriver: driver not supported→ storage driver 配置错误回到 3.2 节检查 daemon.json最后分享一个血泪经验Ubuntu 16.04 的systemd229 有一个已知 bug当 Runner 服务被kill -9强杀后/run/gitlab-runner目录不会自动清理导致下次启动时报address already in use。解决方案是每次systemctl restart前手动清理sudo rm -rf /run/gitlab-runner sudo systemctl restart gitlab-runner这个细节官方文档不会写但你在生产环境一定会遇到。