1. 项目概述与冲突根源
1.1 冲突是怎么发生的
先说结论:K3s 装好后默认会启动内置的 Traefik Ingress Controller,它作为 DaemonSet 直接监听宿主机的 80 和 443 端口。Harbor 在默认安装配置下,HTTP 流量也走 80 端口。两者放在同一台机器上,必然有一个起不来,或者两个都起不来。
我这次遇到的情况很典型:一台 Ubuntu 22.04 服务器,先装了 K3s,集群一切正常,然后准备用 Docker Compose 方式部署 Harbor,结果 docker compose up -d 之后,Harbor 的 nginx 容器反复重启,日志里明确写着 bind: address already in use。查了 ss -lntp,发现 80 端口已经被 K3s 的 svclb-traefik 进程占着。
这里要稍微解释一下 K3s 的架构,方便你理解为什么它会“抢”端口。K3s 默认集成了 Traefik 作为集群的 Ingress Controller,而 K3s 为了让流量能直接进入集群,会让 Traefik 不只监听集群内部的 VIP,而是直接绑定宿主机的 80/443。更关键的是,K3s 的 svclb(Service Load Balancer)会在宿主机上起独立进程来转发端口,这个进程不是普通的容器端口映射,它直接绑定网络命名空间。所以即使你想着“K8s 的 Ingress 不占用宿主机端口吧”,对不起,K3s 不是这么干的。
1.2 影响范围与解决思路
这个问题的影响范围比你想象中大。Harbor 是我们内网 CI/CD 的核心,所有镜像的推送和拉取都要走它。而 K3s 又是现成的容器编排平台,两者必须共存。如果端口的冲突不解决,后续 Jenkins、GitLab Runner 这些工具都会受影响。
解决思路其实只有两条:要么让 K3s 让出 80 端口,要么让 Harbor 换端口。但端口不是随便换的,因为 Docker 客户端在拉取镜像时,默认走 443/80 端口,如果你把 Harbor 的 HTTP 端口改成 8080,那么所有 docker pull harbor.internal:8080/... 的指令都得显式带端口,K8s 的 imagePullPolicy 虽然不受影响,但每个节点的 Docker 都得配置 insecure-registries。选择哪条路,取决于你对集群的要求,以及对后期的扩展规划。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 方案选型:三条路线的优劣对比
2.1 方案 A:让 Traefik 让位,Harbor 独占 80
这个方案的核心是禁用或改造 K3s 内置的 Traefik,让集群 Ingress 改用 NodePort 方式对外提供服务,这样 80/443 端口就完全释放给 Harbor。
优势很明显:Harbor 可以保持默认端口,所有 Docker 客户端无需额外配置,直接按 http://harbor.internal 访问即可。对于内网环境来说,这就是最省事的路径。劣势在于你要动 K3s 的默认组件,如果以后想用 Ingress 对外暴露 Web 服务,就必须在 Service 里指定 NodePort,端口范围只有 30000-32767,访问时 URL 上要带一个不太美观的端口号。
这个方案适合什么场景? 适合 Harbor 是这台机器“第一公民”的场景,部署重心是镜像仓库,K3s 只是顺带跑一些消费者应用。
2.2 方案 B:改 Harbor 端口,K3s 保持不动
这个方案反着来,K3s 完全不动,Harbor 把 HTTP 端口改成 8080,HTTPS 端口改成 8443。如果你使用 Docker Compose 方式部署 Harbor,修改起来很方便:改 harbor.yml 里的 http.port 即可,然后重新执行 prepare 和 install.sh。
优势是 K3s 保持原样,以后想用 Ingress 对外提供服务,什么配置都不用改,很省心。劣势集中在客户端侧:所有要拉取 Harbor 镜像的机器,必须把 harbor.internal:8080 加入到 Docker 的 insecure-registries 列表,因为非 443 端口的 HTTP Registry 默认是不被信任的。在内网环境里这问题不大,但如果遇到平台限制(比如某些云厂商的 K8s 节点不允许改 Docker daemon),就会很麻烦。
这个方案适合什么场景? 适合后续会大量使用 K3s Ingress 暴露 Web 服务的场景,Harbor 是“配合角色”。
2.3 方案 C:Nginx 反向代理,两边都不改
这是第三条路:K3s 和 Harbor 都保持默认端口,在宿主机上装一个 Nginx 监听 80/443,然后按域名或路径把流量分流到 Harbor 或 Traefik。
Nginx 的配置大致是:harbor.internal 转发到 127.0.0.1:8080(Harbor 改端口后),*.cluster.internal 转发到 127.0.0.1:80(Traefik)。这样 K3s 的 Traefik 也占不了 80,Harbor 也保持统一入口。
但这条路有个前提:你得先给 Harbor 换端口,否则 Nginx 也没法同时代理两个都在 80 端口监听的服务。而且内网 DNS、证书都要统一管理,复杂度并没有降低多少。我觉得这条路适合“端口极其紧张、调度非常灵活”的复杂环境,对于单一服务器部署来说,有点杀鸡用牛刀。
我的选择:方案 A。 理由很简单:我的这台服务器主要服务对象就是 Harbor,K3s 跑的应用不需要对外暴露,只需要能从集群内访问 Harbor 就行了。这样 K3s 就能把 80 端口完全让出来,集群内部的应用通过 Service 直接访问 Harbor,完全绕开了端口问题。
3. 实操核心:方案 A 全流程
3.1 让 K3s 的 Traefik 从 80 端口退场
K3s 的 Traefik 之所以会自动启动,是因为 K3s 在安装时会在 /var/lib/rancher/k3s/server/manifests/ 目录里放一些 “打包” 的清单文件。你会发现这个目录里默认有个 traefik.yaml,它会在集群启动时自动部署 Traefik。想让它不占用宿主机的 80 端口,最直接的方法是编辑这个文件,给 Traefik 的 Deployment 增加一个参数,让它的 service 不再创建 LoadBalancer 类型,或者直接改成 NodePort。
但实测下来,光改这个文件不行,因为 K3s 的 svclb 是单独的 DaemonSet 在托管端口监听。我直接说说我验证过的做法:
bash复制vim /var/lib/rancher/k3s/server/manifests/traefik.yaml
找到 spec 部分,把 Service 的类型从 LoadBalancer 改成 NodePort,然后改端口:
yaml复制spec:
service:
spec:
type: NodePort
ports:
- name: web
port: 80
nodePort: 30080
- name: websecure
port: 443
nodePort: 30443
但 K3s 有个机制,manifest 文件是启动时一次性加载的,后期改完不会自动热更新。所以改完文件之后,必须重启 K3s 服务,同时把旧的 svclb 资源清掉。我的操作如下:
bash复制systemctl restart k3s
kubectl delete svc traefik -n kube-system
kubectl delete daemonset svclb-traefik -n kube-system
kubectl delete sa traefik -n kube-system
清理完之后,Traefik 会通过清单重新创建服务,但这时它是 NodePort 类型,不再绑定宿主机的 80 端口。执行 ss -lntp | grep ':80 ' 确认端口已经释放,这一步就算完成了。
注意:如果你不想彻底禁用 Traefik,就按这个方式改。如果想彻底禁用,可以在 K3s 安装的时候加
--disable traefik参数,或者直接把 manifests 目录里的traefik.yaml移走并重启 K3s。
3.2 后续 K3s 应用如何暴露服务
Traefik 变成 NodePort 之后,K3s 内部的 Ingress 还能不能用了?能用,但对外暴露的端口变成了 30080/30443。这个操作本身不复杂,关键是你要提前规划好 NodePort 的端口号。我在测试环境用的是 30080,因为内网没有别的服务占用这个端口段。
如果你有多个服务要暴露,建议做一个端口规划表,避免冲突:
| 服务 | NodePort | 说明 |
|---|---|---|
| Web 控制台 | 30080 | HTTP 流量入口 |
| WebSocket 服务 | 30081 | 长连接服务 |
| Metrics | 30082 | 监控数据 |
这里有个细节:NodePort 的端口监听在宿主机上是直接的 kube-proxy 规则,没有额外的容器绑定,所以性能影响很小。我们内网流量不大,实测下来完全够用。
改完 Traefik 后,我重建了一个简单的 Nginx Deployment 做验证,kubectl get svc 确认 Service 的 NodePort 被分配成功,然后通过 curl http://<宿主机IP>:30080 验证访问正常。
3.3 在 Ubuntu 上部署 Harbor 全过程
现在 80 端口已经释放出来,可以安装 Harbor 了。我的这台测试机是 Ubuntu 22.04,Docker 版本是 24.0.x,Docker Compose 用的是 v2 语法。Harbor 这里有个埋坑点:Harbor 的离线安装包虽然给出了 harbor.yml.tmpl,但直接用 prepare 是会报 config validation 错误的,因为这个模板是官方给你做参考的,很多参数默认值就是空或者带占位符的。
我的安装步骤如下:
bash复制# 下载解压 harbor-offline-installer
wget https://github.com/goharbor/harbor/releases/download/v2.10.0/harbor-offline-installer-v2.10.0.tgz
tar xzvf harbor-offline-installer-v2.10.0.tgz
cd harbor
cp harbor.yml.tmpl harbor.yml
vim harbor.yml
关键配置如下:
yaml复制hostname: harbor.internal
http:
port: 80
https:
port: 443
certificate: /data/cert/harbor.internal.crt
private_key: /data/cert/harbor.internal.key
harbor_admin_password: Harbor12345
database:
password: root123
data_volume: /data/harbor
提示:如果以后要用 HTTPS,证书路径必须在
prepare之前就存在,否则prepare校验会失败。我的测试环境暂时只开 HTTP,所以把https整个段落注释掉了,但注意harbor.yml里https和http至少要启用一个。
然后执行:
bash复制sudo ./prepare
sudo ./install.sh
prepare 会生成 docker-compose.yml 以及各个组件的配置。如果 prepare 不报错,install.sh 就会拉起一套完整的 Harbor 服务,包括 nginx、portal、core、jobservice、registry、registryctl 还有数据库和 Redis。
启动完成后,我打开浏览器访问 http://harbor.internal,能正常弹出登录页,然后用 admin/Harbor12345 登录,说明 Harbor 已经成功占用 80 端口。这时候再回头检查 ss -lntp | grep ':80 ',能看到的就是 harbor 的 nginx 进程了。
3.4 Docker 客户端与 K3s 拉取 Harbor 镜像的配置
Harbor 装好后,接下来要让集群里的 Pod 能拉取私有镜像。没有这一步,后续你推上去的镜像在 K3s 节点上拉不下来,报错是 image pull backoff 或者 401 Unauthorized。
K3s 中的 Docker 和原生 Docker 一样,需要配置 insecure-registries。我在 /etc/docker/daemon.json 里加了一段:
json复制{
"insecure-registries": ["harbor.internal"]
}
然后重启 Docker:
bash复制sudo systemctl restart docker
但是注意,K3s 的默认容器运行时是 containerd,不是 Docker。你要让 K3s 里的 Pod 能拉 Harbor 镜像,得把 containerd 的配置也加进去。K3s 的 containerd 配置在 /var/lib/rancher/k3s/agent/etc/containerd/config.toml,但这个文件是 K3s 自动生成的,不能直接改,改了也会被覆盖。正确做法是创建一个单独配置文件:
bash复制vim /var/lib/rancher/k3s/agent/etc/containerd/config.toml.tmpl
写入如下内容:
toml复制[plugins."io.containerd.grpc.v1.cri".registry.mirrors."harbor.internal"]
endpoint = ["http://harbor.internal"]
然后重启 K3s:
bash复制systemctl restart k3s
重启完成后,在 K3s 节点上用 ctr images pull harbor.internal/library/busybox:latest 测试一下。如果还是拉不下来,可能是 harbor.internal 这个域名解析到了公网,内网 DNS 没配,那就先改成 IP 或者改 hosts 文件。
改完 containerd 之后,还要处理 镜像仓库的认证。K8s 里 Pod 拉私有仓库的镜像不会默认复用 Docker 的登录凭证,需要手动创建 imagePullSecret:
bash复制kubectl create secret docker-registry harbor-secret \
--docker-server=harbor.internal \
--docker-username=admin \
--docker-password=Harbor12345 \
--namespace=default
然后在 Deployment 的 spec 里引用:
yaml复制spec:
template:
spec:
imagePullSecrets:
- name: harbor-secret
4. 实操核心:方案 B 全流程(备用)
4.1 修改 Harbor 监听端口并重建
如果你看完前面的分析,决定采用方案 B,让 K3s 不动,Harbor 换端口,那操作也不复杂。以 Harbor 2.10 为例,修改 harbor.yml:
yaml复制hostname: harbor.internal
http:
port: 8080
https:
port: 8443
然后重新执行:
bash复制sudo ./prepare
sudo ./install.sh
注意 install.sh 会重启 Harbor 的 docker-compose 服务,所以要等它跑完。之后访问就需要带端口了:http://harbor.internal:8080。
这个过程中最容易踩的坑是:改完端口后,Docker 客户端如果不更新 insecure-registries,推镜像会超时。因为 Harbor 监听 8080,但 Docker 客户端默认会先尝试 HTTPS 443,失败后再尝试 HTTP 80,根本不会主动去试 8080。所以必须提前在每台需要推送/拉取镜像的机器上更新 daemon.json:
json复制{
"insecure-registries": ["harbor.internal:8080"]
}
然后重启 Docker。这个步骤一旦漏了,你后面 docker push harbor.internal:8080/... 百分之百失败。
4.2 遇见的 config validation 问题与排查过程
好多人在 Ubuntu 上部署 Harbor 时都栽在 prepare 这一步,报错五花八门,但最常见的就是 config validation 错误。我整理了一份问题对照:
| 报错关键词 | 原因 | 解决办法 |
|---|---|---|
invalid config: the protocol is https but attribute ssl_certificate is not set |
http 被注释,https 没配证书 |
改回启用 http,或补上证书路径 |
cannot load certificate |
证书文件不存在或权限不对 | 确保证书路径存在,并给 harbor 用户可读权限 |
invalid argument |
hostname 填了 IP 或非法域名 |
改成合法域名或用 IP |
the protocol is https but attribute https.certificate undefined |
https 配置里缺 certificate |
检查缩进,证书路径别写 ~ 符号 |
data_volume is not absolute path |
data_volume 没写成绝对路径 |
改成 /data/harbor |
我这台机器上遇到过最“莫名其妙”的错误是 the protocol is https but attribute ssl_certificate is not set。原因是我把 https 整个段落注释掉之后,prepare 还是在找证书,后来发现因为我用了旧版的 harbor.yml.tmpl,里面的 https 段落注释不完整,有一部分内容还留在了配置里。解决办法很简单:删掉不要的内容,不要学人家在线上的 yml 里搞“注释大法”,直接用一个干净的 harbor.yml。
还有一个容易忽视的:prepare 执行前,data_volume 指向的目录必须存在且属主正确。如果你用 sudo ./prepare 执行,但那是个普通用户创建的目录,后面 Harbor 容器启动时因为权限访问不了数据卷,日志里会显示 Permission denied。我习惯提前执行:
bash复制sudo mkdir -p /data/harbor
sudo chown -R 10000:10000 /data/harbor
5. 常见问题与排查技巧实录
5.1 Harbor 启动后容器反复重启
我在这次部署中遇到过一次比较头疼的问题:Harbor 的 nginx 容器反复重启,但 prepare 和 install.sh 都没有报错。查日志发现 nginx 容器里也被映射了 80 端口,而宿主机的 80 端口虽然已经被 Traefik 释放,但 Docker 的 nginx 容器配置想绑定 443 端口时,发现端口也被占用了。问题出在 harbor.yml 里我保留了 https 的 443 端口配置,但宿主机上还有另一个服务监听 443。
解决的办法是:在配置里把 https 完全注释或删除,只留 http。如果你既需要 HTTPS 又需要 HTTP,那必须在 harbor.yml 里把它们区分开,比如 HTTP 8080,HTTPS 8443,然后让 Nginx 统一按端口转发。
排查容器翻转的通用命令是:
bash复制docker ps -a
docker logs <container_name> --tail 50
日志里通常会有 bind: address already in use 或 Permission denied 这种关键信息。前者是端口占用,后者是数据卷权限,两个方向排查很快。
5.2 K3s 的 svclb 进程还在占用 80 端口
有一种情况是,你明明改了 Traefik 的 yaml 并重启了 K3s,但 ss -lntp 还是能看到 svclb-traefik 在监听 80 端口。这是因为 K3s 的 manifests 机制里,旧的 svclb DaemonSet 没有被删除,而新生成的 svclb 又重新绑定在同一个端口上。
我的建议是,在修改 traefik.yaml 之前,先手动把 svclb-traefik 这个资源通过 kubectl 删掉,再重启 K3s。不要嫌麻烦,先清理再重启是最干净的方案:
bash复制kubectl delete ds svclb-traefik -n kube-system
systemctl restart k3s
还有一个坑:如果你用的 K3s 版本比较旧,可能 traefik.yaml 里改端口的方式不一样,我记得 1.23 之前是直接编 svc 的 spec.ports[*].nodePort,1.24 之后变成了 spec.service.spec.ports 结构。改之前先用 kubectl get svc traefik -n kube-system -o yaml 看看现网结构,照葫芦画瓢,比自己凭记忆改靠谱。
5.3 K3s 拉取 Harbor 镜像时报 401 Unauthorized
部署完 Harbor 后,我用 ctr images pull 测试拉取镜像时报 401。这让我排查了很久,因为 docker login 已经做过了。
问题出在 ctr 和 K8s 的 CRI 集成方式不同:K3s 的 containerd 不读 Docker 的 ~/.docker/config.json,它要的凭证是放在 containerd 自己的命名空间里的,或者通过 imagePullSecret 传给 kubelet。所以光有 docker login 没用,得用 kubectl create secret docker-registry,或者把认证信息放到 containerd 配置的 registry.configs 里。
toml复制[plugins."io.containerd.grpc.v1.cri".registry.configs."harbor.internal".auth]
username = "admin"
password = "Harbor12345"
auth = ""
这个 auth 字段是 base64 编码的 user:pass,其实不用手填,username 和 password 填了就行,containerd 会自己处理。改完后重启 k3s 再试。
如果集群里多个命名空间都要用 Harbor,建议在每个命名空间都建一个 secret,避免权限问题。
6. 总结与经验:我实际部署中的体会
回头再看这个“K3s + Harbor 端口冲突”的问题,其实核心就一句话:K3s 默认的 Traefik 不是江湖传说中的“纯集群内部组件”,它会真刀真枪地绑定宿主机端口。理解了这个,你就知道为什么 Harbor 和它不能同居一室了。
我个人在实际操作中的体会是,如果你跑的是单节点 K3s,并且这台机器的核心任务就是做镜像仓库,那么方案 A 是最省心的。虽然改 Traefik 端口看起来像是“动了集群根基”,但实际上你只是把它的对外暴露方式从宿主机端口换成了 NodePort,K8s 的应用扩展、Ingress 路由这些功能完全不受影响。而且遇到要暴露集群内部服务的情况,用 NodePort 也能访问,只是需要多带个端口号而已。
如果哪天你的集群规模变大了,Web 应用需要统一的 80/443 对外入口,那再考虑把 Traefik 拿回来。到时候 Harbor 仍然保留 80 端口,外部入口换成云平台的负载均衡器,或者在前端加一层 Nginx,这就回归到常规的架构了。
最后再分享一个小技巧:在 K3s 里配置 containerd 拉取私有仓库时,如果你改了 /var/lib/rancher/k3s/agent/etc/containerd/config.toml.tmpl 但重启后配置被重置了,可能是文件权限的问题。确认它的属主是 root,并且没有被 K3s 的自动化备份文件覆盖。这种问题很隐蔽,我踩过三次坑之后才学会检查 journalctl -u k3s 里的 containerd 日志,那里基本能看到所有配置加载的错误线索。
