简介:面向ARM64架构服务器的Harbor v2.9.0离线安装包,专为内网或离线环境下搭建私有Docker镜像仓库的运维与开发人员准备。内容取材自Harbor官方离线安装器,需提前装好Docker和Docker Compose,能显著减少在线拉取镜像失败、手工拼接多步部署的麻烦。压缩包整体约722.35MB,共6个文件,主要包括两个Shell脚本(一键安装与公共函数)、离线镜像压缩包、Harbor配置模板、prepare初始化工具及LICENSE许可证;解压后可根据实际环境调整监听端口、数据目录等关键配置,再通过脚本完成安装启动。除镜像与脚本外,压缩包还附带配置模板和基础工具,便于在有防火墙或完全断网的ARM64环境中快速获得可访问的Harbor服务,省去多源下载与版本匹配工作。已有969人学习下载,适合具备基础Docker使用经验、需要离线交付Harbor的中高级使用者。
1. Harbor v2.9.0 离线装到 ARM64:先别急着解压官方离线包
ARM64 服务器(鲲鹏 920、飞腾、Graviton 这类 arm64 环境)上离线部署 Harbor 2.9.0,最磨人的不是 Harbor 本身的配置,而是把「arm 架构」和「离线」两个条件叠在一起。很多人的第一反应是去 GitHub Releases 下载 harbor-offline-installer-v2.9.0.tgz,传到目标机上解压、跑 ./install.sh 一把梭,结果 docker ps 里所有容器都在 CrashLoopBackOff,docker logs 一翻全是 exec format error。原因很直接:官方离线包内置的 harbor.v2.9.0.tar.gz 是按 amd64 打好的镜像包,docker load 时不检查架构,容器一跑就露馅。真正要在 ARM 上离线装 Harbor,得自己按 linux/arm64 重新拉取组件镜像、重新打包,再借用官方安装脚本走完配置和启动。这篇笔记就从「造包」讲起,一路到 harbor.yml 配置、部署、客户端验证和排错,适合内网交付、麒麟/openEuler 这类没有外网的 ARM 现场,参数给到可以直接抄。
2. 自己打 ARM64 镜像包:--platform 拉取、重打包、替换 install.sh 的加载源
2.1 为什么官方离线包在 ARM 上必挂
先拆开官方离线包看组成:解压 harbor-offline-installer-v2.9.0.tgz 之后,核心是三个东西——harbor.v2.9.0.tar.gz(一整包 docker save 出来的镜像)、prepare(负责生成 docker-compose.yml 和全套配置的 Go 二进制)、install.sh(依次做镜像加载、配置生成、容器启动)。问题就出在 harbor.v2.9.0.tar.gz 里所有镜像的 manifest 都是 linux/amd64。
docker load 这个操作本身不校验 CPU 架构,它只是把 layer 解出来写进本地镜像库,所以加载阶段不会有任何报错;真正死在半路的是 run 的时候,runc 在 arm64 内核上加载 amd64 的 ELF,直接抛 exec format error。这就是为什么很多人栽在「所有步骤都显示成功,但容器就是起不来」的迷惑状态上。反过来看,Harbor 官方把各个组件镜像都发布成了多架构 manifest,同一个 tag 下同时有 amd64 和 arm64,所以我们完全可以用一条 docker pull --platform linux/arm64 把对应架构的镜像拉下来。这里有个细节:在 x86 联网机器上拉 arm64 镜像不需要装 qemu、不需要 buildx,因为 docker pull 只是按 manifest 选择对应架构的 layer 存到本地,只有 run 才要求 CPU 架构匹配。所以造包这一步,随便找一台能联网的机器都能干。
2.2 锁定 v2.9.0 的组件镜像清单
Harbor 不是单个容器,是一整套互相依赖的服务。离线包里对应镜像如下,打包前先把清单核对一遍,缺一个后面 install.sh 大概率在 docker-compose up 阶段报 image not found。
| 镜像名 | 服务角色 | 是否必选 |
|---|---|---|
| goharbor/harbor-core:v2.9.0 | 核心 API 与 Web 入口 | 必选 |
| goharbor/harbor-jobservice:v2.9.0 | 异步任务(复制/清理/GC) | 必选 |
| goharbor/harbor-portal:v2.9.0 | 前端页面 | 必选 |
| goharbor/harbor-registry:v2.9.0 | 镜像存储(基于 distribution) | 必选 |
| goharbor/harbor-registryctl:v2.9.0 | registry 控制面 | 必选 |
| goharbor/harbor-log:v2.9.0 | 日志聚合容器 | 必选 |
| goharbor/nginx-photon:v2.9.0 | 入口反向代理 | 必选 |
| goharbor/harbor-db:v2.9.0 | PostgreSQL 元数据库 | 必选 |
| goharbor/redis-photon:v2.9.0 | Redis 缓存 | 必选 |
| goharbor/harbor-exporter:v2.9.0 | 指标导出 | metrics.enabled 开启时需要 |
| goharbor/trivy-adapter-photon:v2.9.0 | 漏洞扫描 | install.sh 带 --with-trivy 时需要 |
有一点要提醒:个别 tag 的某些-photon镜像可能没发 arm64 变体,拉取时报no matching manifest for linux/arm64。遇到这种情况先确认该组件能不能关掉(比如 trivy),不能关就换一个接近的次版本 tag 自己测,这是我踩过的一次真实翻车。
2.3 在联网机器上拉取并按 arm64 打包
造包脚本我一般这么写,整段可以直接存成 build-arm64-bundle.sh 用:
#!/usr/bin/env bash set -euo pipefail HARBOR_VERSION=v2.9.0 PLATFORM=linux/arm64 OUT_TAR=harbor.v2.9.0.arm64.tar IMAGES=( "goharbor/harbor-core:${HARBOR_VERSION}" "goharbor/harbor-jobservice:${HARBOR_VERSION}" "goharbor/harbor-portal:${HARBOR_VERSION}" "goharbor/harbor-registry:${HARBOR_VERSION}" "goharbor/harbor-registryctl:${HARBOR_VERSION}" "goharbor/harbor-log:${HARBOR_VERSION}" "goharbor/nginx-photon:${HARBOR_VERSION}" "goharbor/harbor-db:${HARBOR_VERSION}" "goharbor/redis-photon:${HARBOR_VERSION}" "goharbor/harbor-exporter:${HARBOR_VERSION}" "goharbor/trivy-adapter-photon:${HARBOR_VERSION}" ) for image in "${IMAGES[@]}"; do docker pull --platform "${PLATFORM}" "${image}" done # install.sh 里的 docker load 只认一个文件,所以全部镜像必须一次性 save 成单 tar docker save -o "${OUT_TAR}" "${IMAGES[@]}" sha256sum "${OUT_TAR}" > "${OUT_TAR}.sha256"循环里逐张拉取是为了看清楚哪张镜像缺 arm64 变体,别用docker pull -a一把梭,那样拉回来的 tag 列表不可控。--platform linux/arm64是这次打包最关键的一个参数,它决定 manifest 选择;docker save -o指定输出文件名,install.sh 后面加载的就是这个文件。导出后顺手算一个 sha256,传输完比对,避免拷贝过程中 tar 被截断——离线安装最怕文件静默损坏,校验这一步别省。
2.4 目标机上加载与架构校验
tar 传到目标机后,先手动 load 一次并核对架构,再决定要不要替换官方包:
docker load -i harbor.v2.9.0.arm64.tar docker image inspect goharbor/harbor-core:v2.9.0 --format '{{.Os}}/{{.Architecture}}' # 期望输出:linux/arm64{{.Os}}/{{.Architecture}}这个模板能直接打出镜像的真实平台,输出 linux/arm64 才算过了第一关。确认无误后,把这份 tar 复制到离线包解压目录、改名成harbor.v2.9.0.tar.gz覆盖官方那份 amd64 包,install.sh 加载时才会用到你的 arm64 镜像。这里最容易出现的认知误区是:认为install.sh里的docker load -i harbor.v2.9.0.tar.gz会读取解压目录下同名文件,所以文件名必须严格一致;如果你直接用了官方包又没覆盖,前面所有造包工作等于白做。
3. harbor.yml 与部署流程:六个必改字段和 prepare/install.sh 的执行顺序
3.1 harbor.yml 里真正要改的六个字段
官方离线包解压后自带 harbor.yml.tmpl,先 cp 一份成 harbor.yml 再改。下面是针对离线 ARM 场景我要动的字段,注释写在旁边:
# harbor.yml(基于 harbor.yml.tmpl 修改,只列必动项) hostname: 192.168.209.133 # 浏览器和客户端访问用的地址,写成 IP 或域名,别写 localhost http: port: 80 # 离线内网不开 https,用 80 即可 # https: # port: 443 # 官方模板默认注释,http 场景保持注释状态 harbor_admin_password: Harbor12345 # 初始管理员密码,至少 8 位且含大小写字母和数字 database: password: root123 # postgres 密码,部署后不能再从 yml 直接改 data_volume: /data/harbor # 镜像、数据库、redis 全在这一目录,容量按仓库量 2~3 倍预留 log: level: info local: rotate_count: 20 # 容器日志保留份数 rotate_size: 200M # 单份日志上限 trivy: skip_update: true # 离线环境:禁止启动时联网拉漏洞库 offline_scan: true jobservice: max_job_workers: 10 # 复制/GC 任务并发,按机器核数调整hostname 这一项直接影响 nginx 反代配置和客户端访问,改成部署机的实际 IP;如果后面要换 IP,必须重跑 prepare,不是只改 yml 就生效。harbor_admin_password和database.password是两个独立密码,前者是登录 UI 的管理员密码,后者是 postgres 的认证密码,prepare 会把 database.password 写进 postgres 初始化脚本,所以部署完再改 yml 里的 database 密码不会同步到数据库,属于「没有后悔药」的字段,动手前想清楚。data_volume在 ARM 设备上尤其要留意——很多 ARM 服务器系统盘不大,镜像仓库数据全在 data_volume 下,我给它的容量建议是预计仓库占用量的 2 到 3 倍,另外确认这块目录所在分区别和日志分区挤在一起。
3.2 prepare 和 install.sh 各自在做什么
prepare 是一个 Go 编译的二进制,职责是用 harbor.yml 生成 docker-compose.yml,以及 common/config 下 nginx、redis、postgres、证书目录的全套配置。这意味着:之后任何对 harbor.yml 的修改,都必须重新执行 ./prepare 再 docker compose up -d,直接手改 common/config 里的文件是无效的,下次 prepare 一跑就被覆盖。建议把 prepare 理解成「配置编译」这一步,它不启动任何容器,只产出配置。
install.sh 做的是另一件事:先做环境检查(docker 版本、compose 版本),再 docker load 镜像包,然后调 prepare,最后 docker compose up -d 把整个服务拉起。Harbor 2.9 对 compose 版本有硬校验,Docker 建议 20.10.10 以上、compose 用 v2(2.3.0 及以上);如果目标机只有 docker-compose 1.29 或者没装 compose 插件,install.sh 会直接退出,提示 version not supported。离线环境里装 compose 插件的常规做法是把 docker-compose-linux-aarch64 这份二进制拷到 /usr/local/bin,再 chmod +x,或者安装官方 compose-plugin 的 rpm/deb 包。
还有一个执行顺序的细节:install.sh 内部会先判断镜像是否已存在,不存在才去 load。所以我们提前手动docker load过一次没有问题,属于幂等操作;但如果你既手动 load 过旧的 amd64 包、又忘了覆盖同名 tar,install.sh 检查镜像已存在就会跳过加载,最后跑起来还是 amd64 镜像,这个坑在 5.1 里会细说。
3.3 执行部署并核对容器状态
全部就绪后按下面顺序跑:
cd ./harbor chmod +x install.sh prepare ./install.sh --with-trivy # 看到 "Harbor has been installed and started successfully" 才算结束 docker compose ps docker logs harbor-core --tail 20--with-trivy会要求 trivy-adapter-photon 镜像存在,并把它加进 docker-compose.yml 的服务列表;离线环境下如果没把握搞定漏洞库,我一般建议第一遍先不带这个参数跑通,后面真有扫描需求再补配。部署完成后核对两处:一是docker compose ps里所有服务 State 都是 Up,不能有 Restarting;二是docker logs harbor-core --tail 20里没有报 ERROR 刷屏。容器名固定是 harbor-core、harbor-db、harbor-registry 这一套前缀,不会带随机后缀,排错时直接按名字取日志就行。
4. 客户端接入验证:daemon.json、docker login 与第一张 ARM64 镜像的 push/pull
4.1 客户端连不上的根因:docker 默认走 https
服务端起来了,客户端 docker push 却报错,问题十有八九不在 Harbor 而在 docker 客户端的协议假设。docker 对 registry 地址默认按 https 访问;Harbor 只开了 http:80 时,客户端docker push 192.168.209.133/library/test会先尝试https://192.168.209.133/v2/,握手失败后如果该地址不在 insecure-registries 白名单里,直接放弃报错;即使在白名单里,也要等一次 https 超时才能降级到 http,体验很差。所以正确做法是让 docker 明确知道「这个地址允许明文 HTTP、不做证书校验」,这就是insecure-registries的用途。
这里有个惯性坑:很多人觉得既然是 http,把 daemon.json 配了 insecure 就能连,但没注意 tag 里写没写端口。docker 对不带端口的主机默认走 443 的 https,即便 insecure 允许降级,也要先碰一次壁。我统一要求 tag 和 daemon.json 都写全192.168.209.133:80,让 docker 直接按 http 明文访问,少一次无谓的失败。
4.2 insecure-registries 的配置与重启注意
{ "insecure-registries": ["192.168.209.133:80"] }改完必须重启 docker 服务才生效,这一点容易被忽略:
sudo systemctl restart docker注意:重启 docker 会把本机所有容器一起重启一遍,包括非 Harbor 的业务容器。线上机器操作前先确认业务容器的 restart 策略,挑维护窗口执行。
daemon.json 是守护进程级配置,不是某个容器的环境变量,所以不存在「改完热加载」这种说法。配置项里写的是 host:port 完整串,和后面docker push命令里的地址要严格一致,docker 按镜像引用里的 registry 地址去匹配白名单,串对不上照样算 insecure 不生效。
4.3 从 login 到 push/pull 的完整命令
在一台能和 Harbor 互通 TCP 80 的客户端机器上执行:
docker login 192.168.209.133:80 -u admin -p Harbor12345 # 期望输出:Login Succeeded docker tag busybox:latest 192.168.209.133:80/library/test-arm64:v1 docker push 192.168.209.133:80/library/test-arm64:v1 # 期望输出:latest digest: sha256:... size: ... docker pull 192.168.209.133:80/library/test-arm64:v1 docker images --digests 192.168.209.133:80/library/test-arm64tag 里/library/是 Harbor 预置的公开项目,push 前要保证 project 已存在并且用户名有该项目的推送权限;新建项目时注意项目名只允许小写字母、数字和连字符,这是 Harbor 的硬性命名规则。pull 回来后用docker images --digests比对 digest 是否和 push 输出一致,能确认存储链路完整。push 时如果报 denied,优先查用户名是否在项目成员列表里,其次是项目名拼写。
4.4 验证清单:ping 接口、UI 与镜像回拉
一通操作后,我习惯按固定顺序过一遍才算交付完成:
接口层验证用 curl 探 Harbor 的 API:
curl -fsS http://192.168.209.133/api/v2.0/ping # 期望输出:PongUI 层验证打开http://192.168.209.133,用 admin 登录后看项目列表和镜像列表是否展示正常;镜像层验证就是 4.3 里那套 push/pull 回环。三层全过,Harbor 才算真正可用,缺一层都说明还有隐性问题没暴露。
5. 避坑:ARM 离线部署 Harbor 的五个常见翻车点
5.1 容器反复重启:exec format error
现象:install.sh 全程没报错,docker ps 看到 harbor-core、harbor-registry 等反复 Exited (1),docker logs harbor-core 末尾是exec format error,或者 older 的报错样式是standard_init_linux.go: exec user process caused: exec format error。
原因:docker load 成功不代表镜像能跑。要么是没覆盖官方 amd64 包,install.sh 把旧的 amd64 镜像 load 进来了;要么是你手动 load 过旧包导致 install.sh 认为镜像已存在、跳过加载。
解决:先docker image inspect --format '{{.Os}}/{{.Architecture}}' goharbor/harbor-core:v2.9.0,逐个核对 11 个镜像全是 linux/arm64;确认替换后的 tar 文件名和 install.sh 里 load 的路径完全一致;不放心就直接手动docker load -i新包再跑 install.sh,加载本身幂等,多跑一次不损失什么。
5.2 prepare 脚本本身也是 amd64:又见 exec format error
现象:在 ARM 目标机上直接./prepare,报exec format error;或者 install.sh 跑到 prepare 那一步就中断,日志同样指向 exec format error。
原因:官方离线包里带的 prepare 是 linux/amd64 编译的 Go 二进制,v2.9.0 这个版本我没有在离线包里找到 arm64 变体。镜像包你换了 arm64,脚本二进制还是 amd64,这一步就卡死了。
解决:从源码在 ARM 环境重编一个。拉 v2.9.0 源码后,prepare 的 main 包在 tools/prepare 目录(不同 tag 可能挪过位置,用find . -name main.go | grep prepare定位),进去执行GOOS=linux GOARCH=arm64 go build -o prepare .,把产物覆盖回解压目录,file prepare输出 aarch64 即可。没有 go 工具链的机器,可以在一台联网 ARM 机上编好再拷过去——Go 的交叉编译不需要 qemu,这是 Go 相对其他语言的省心之处。
5.3 客户端 push 报 dial tcp:协议、端口与防火墙
现象:客户端docker push 192.168.209.133:80/library/test报Get "https://192.168.209.133/v2/": dial tcp 192.168.209.133:443: connect: connection refused,注意错误里是 443 不是 80。
原因:该报错说明客户端按 https 访问了 443,和 Harbor 实际监听的 http:80 对不上;另外也可能是 Harbor 容器没起来、或者宿主防火墙没放行 80,导致 TCP 直接拒绝。
解决:先curl -v http://192.168.209.133/v2/从客户端探一次。能通说明服务正常,问题在 docker 协议,把 daemon.json 的 insecure-registries 配成带 :80 的完整串;curl 不通就先看 Harbor 容器状态和ss -lntp确认监听,再看firewall-cmd --list-ports或 iptables 有没有放行 80/tcp。openEuler 和麒麟的默认防火墙策略都不一致,这条排查顺序能省掉很多无效操作。
5.4 harbor-db 起不来:data_volume 属主与 SELinux
现象:harbor-db 或 redis 容器一直 CrashLoopBackOff,日志里是Permission denied,postgres 报could not create directory "/data/harbor/database"。
原因:Harbor 容器内部默认以 uid 10000 运行,data_volume 挂载目录的属主不是 10000,容器内进程没权限建目录写数据;在 openEuler、麒麟这类默认开启 SELinux 的系统上,还要多查一层文件安全上下文。
解决:先mkdir -p /data/harbor && chown -R 10000:10000 /data/harbor;SELinux 环境再执行chcon -Rt container_file_t /data/harbor,或者临时setenforce 0定位是不是 SELinux 拦截,确认后按实际策略放行。data_volume 如果挂在 NFS 上,还要检查挂载参数有没有 no_root_squash,否则即使是 root 也会被 squash 成 nobody。
5.5 漏洞扫描永远卡住:trivy 离线库没着落
现象:UI 里对镜像发起扫描,任务一直 Queued 或失败,trivy-adapter 日志停在updating the vulnerability database或下载超时。
原因:trivy-adapter 首次扫描需要拉取 trivy-db 漏洞库,离线环境拉不到,任务就一直挂着不结束。
解决:部署时先不带 --with-trivy,把 Harbor 主链路跑通;确实需要扫描的,在联网机器上把 trivy-db 镜像拉好 save 出来一起导入,并在 harbor.yml 里设trivy.offline_scan: true和trivy.skip_update: true,让 adapter 不联网、直接用本地库。这两个开关缺一个都可能出现「任务显示成功但扫描结果为空」的假象。
6. 把部署固化成脚本:一条命令重装并跑完端到端验证
部署次数多了以后,我习惯把「镜像加载 → hostname 注入 → prepare → install.sh → 健康检查」全部写进一个脚本,重装机器或换节点时一条命令兜底,省得每次手工敲、漏一步就靠排错经验补。脚本大概长这样:
#!/usr/bin/env bash set -euo pipefail HARBOR_ROOT=${1:-/opt/harbor} HARBOR_IP=${2:-192.168.209.133} HARBOR_PASS=${3:-Harbor12345} IMAGE_TAR=harbor.v2.9.0.arm64.tar # 1) 镜像加载:幂等,重复执行不报错 docker load -i "${HARBOR_ROOT}/${IMAGE_TAR}" # 2) hostname 注入:harbor.yml 没改就用 sed 兜底 cd "${HARBOR_ROOT}" sed -i "s/^hostname: .*/hostname: ${HARBOR_IP}/" harbor.yml # 3) 配置生成 + 部署启动 ./prepare ./install.sh # 4) 端到端验证:容器状态 + API 探活 STATE=$(docker compose ps --format '{{.State}}' | sort -u) [ "$STATE" = "Up" ] && echo "PASS: 所有容器状态为 Up" || { echo "FAIL: $STATE"; exit 1; } PONG=$(curl -fsS "http://${HARBOR_IP}/api/v2.0/ping") [ "$PONG" = "Pong" ] && echo "PASS: API ping 正常" || { echo "FAIL: ping 异常"; exit 1; }脚本里刻意把docker compose ps的状态去重后和 Up 比较,避免漏看某个 Restarting 容器;curl /api/v2.0/ping探的是 API 层,比单纯看容器状态更接近真实可用性。这两个检查过了,脚本才算结束。docker compose ps的--format '{{.State}}'是取容器状态字段,sort -u去重后只有一种取值 Up 才通过,任何容器异常都会让结果混入别的值。
验证清单我习惯压成一张表,交付时照着勾:
| 检查项 | 命令 | 期望结果 |
|---|---|---|
| 容器状态 | docker compose ps | 所有服务 Up |
| API 探活 | curl http://IP/api/v2.0/ping | Pong |
| 管理员登录 | docker login IP:80 -u admin | Login Succeeded |
| 真实推送 | docker push IP:80/library/test:v1 | 输出 digest |
| 日志检查 | docker logs harbor-core --tail 20 | 无 ERROR 刷屏 |
关于升级,趁这里多说一句:之后从 2.9 往 2.10 升时,流程还是同一套——先 docker compose down,备份整个 data_volume 和 harbor.yml,再换新版本镜像包重跑 install.sh。2.x 大版本之间的数据库 schema 迁移不可逆,升级前那次备份就是后悔药,我见过不止一个人跳过备份直接升,最后回滚无门只能重新初始化仓库。
从那以后,我每次交付完都会坚持在客户端机器上真实 push 一张镜像,而不是只看 docker compose ps 全绿——容器状态全绿但协议配错、镜像架构不对的情况我踩过不止一次。这套造包、部署、验证的流程走完,Harbor 才算真正交付。希望帮到你。
本文还有配套的精品资源,点击获取