Skip to content
Go back

使用 Docker 本地部署 Rancher 并通过 Tailscale 私网纳管 K3s 集群

| 0 Views Edit page

前言

Rancher 作为 Kubernetes 集群的统一管理平面,在多集群环境下能极大降低运维负担。
这次我计划把它部署在家里的一台内网虚拟机上,通过 Caddy 反代暴露到公网以便出门时也能访问,同时在架构上预留好网络通路:后续可以通过 Tailscale 私有网络将异地的跨机房 K3s 集群直接注册进来,实现统一纳管。


部署架构

这次的机器规划是这样的:

角色主机名公网 IP内网 IPTailscale IP / 域名备注
反代入口aliyun-server-10.64.0.11 / aliyun-server.ts.netCaddy,公网域名 rancher.example.com 解析到这里
Rancher 服务端rancher-192.168.2.110.64.0.12 / rancher.ts.net仅运行 Rancher 容器,只对外通过 Tailscale 暴露
K3s Serverk3s-server-192.168.2.210.64.0.13运行 v1.34.6+k3s1 的现有集群
K3s Agentk3s-agent-01-192.168.2.310.64.0.14运行 v1.34.6+k3s1 的现有集群
K3s Agentk3s-agent-02-192.168.2.410.64.0.15运行 v1.34.6+k3s1 的现有集群

我的 Tailscale 是通过自建的 Headscale 管理的,部署过程参考:部署 Headscale 和 DERP 以通过 Tailscale 实现无公网 IP 服务器之间的组网

一共两条流量路径:

  • 公网访问链路:浏览器 → rancher.example.com(公网 DNS 解析到 aliyun-server 公网 IP) → Caddy → Tailscale 私网 → rancher:8443 → Rancher。
  • Tailscale 私网访问链路:K3s 节点 → rancher.example.com(Headscale MagicDNS 解析到 aliyun-server 的 Tailscale IP) → Caddy → Tailscale 私网 → rancher:8443 → Rancher。

两条路径最终都通过 Caddy 收口,好处是:

  • 公网和私网使用同一个域名 rancher.example.com,所以 Rancher 的 server-url 只有一个值,不会出现登录后被跳转到不可访问地址的问题。
  • TLS 证书只在 Caddy 上签一次(Let’s Encrypt 自动签 rancher.example.com),Rancher 容器只在内部 Tailscale 网络中跑,自签证书即可,无需为它单独管理证书。
  • rancher 完全不暴露公网,安全性更高。

截止 2026/04/23,Rancher 的最新稳定版本为 v2.14.0,它能完整支持 K3s v1.34.x,因此本文以这个版本作为示例。


方案概述

  1. 使用 Docker 部署 Rancher 服务端
    1. 安装 Docker 环境
    2. 准备私有镜像仓库(用于解决境内拉取 rancher/* 镜像困难的问题)
    3. 配置 rancher 的 Docker 默认镜像地址
    4. 启动 Rancher 容器并持久化数据
  2. aliyun-server 上配置 Caddy 反向代理
  3. 首次登录并进行设置
    1. 设置管理员密码
    2. server-url 固定为公网域名
    3. agent-tls-mode 调整为 System Store
  4. 通过 Tailscale 私网将外部 K3s 集群注册到 Rancher
    1. 在 Headscale 中下发自定义 DNS 记录
    2. 在 K3s 节点上验证 DNS 记录已下发
    3. 在 Rancher 中创建导入集群
    4. 在 K3s 集群中执行导入命令

操作步骤

一、使用 Docker 部署 Rancher 服务端

1、安装 Docker 环境

官方的 Docker 安装方式可以参考我的另一篇文章:Ubuntu 20.04 从官方源安装最新的 Docker
或是直接使用下面适配 Ubuntu/Debian 的命令快速安装:

sudo apt-get update -y
sudo apt-get install ca-certificates curl gnupg lsb-release -y
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update -y
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin -y
docker -v

Rancher 官方对于单节点部署推荐使用 Docker 的方式:Installing Rancher on a Single Node Using Docker
但要注意:Docker 单节点部署只适合做测试/家庭实验室,不适合用在生产环境

2、准备私有镜像仓库

我这台 rancher 是在国内,直连 Docker Hub 拉 rancher/* 镜像非常痛苦。
更重要的是,Rancher 的 Docker 容器启动后*会在内部再起一个 K3s 集群来跑自己的组件(Fleet、rancher-webhook、shell 等等),这些组件的镜像同样来自 Docker Hub,如果一启动就卡在镜像拉取上会非常折腾。

因此我会把所有 rancher/* 系镜像全部从自建的 registry.example.com 拉取。
这个仓库是一个 Docker Registry v3 的 pull-through 缓存,部署方式参考:使用 Docker 启动 Registry 镜像加速服务并使用 Caddy 进行 TLS 证书管理

3、配置 rancher 的 Docker 默认镜像地址

为了让所有 docker pull rancher/xxx 都自动走 registry.example.com 的缓存,我们给本机 Docker daemon 加一个镜像加速地址:

sudo nano /etc/docker/daemon.json
{
  "registry-mirrors": [
    "https://registry.example.com"
  ]
}

保存后重启 Docker:

sudo systemctl daemon-reload
sudo systemctl restart docker

设置完之后手动拉一次 Rancher 镜像验证链路是否通了、顺便让第一层镜像提前预热:

docker pull rancher/rancher:v2.14.0

看到输出里出现 Pull completeStatus: Downloaded newer image for rancher/rancher:v2.14.0 就说明链路正常。

注意 registry-mirrors 的作用仅限于宿主机 Docker
Rancher 容器内部那一套 K3s(用的是独立的 containerd)不会读取宿主机 /etc/docker/daemon.json,所以内嵌 K3s 拉取它自己的组件镜像时还是会去 docker.io
这也是下面 docker run 里仍然要显式写 CATTLE_SYSTEM_DEFAULT_REGISTRY=registry.example.com 的原因:把内嵌 K3s 的镜像前缀也一并改写掉。

4、启动 Rancher 容器并持久化数据

创建工作目录和持久化目录,防止之后升级 Rancher 时数据丢失:

mkdir -vp /opt/rancher/data       # 持久化 Rancher 的 etcd 数据
mkdir -vp /opt/rancher/auditlog   # 持久化 Rancher 的 API 审计日志
mkdir -vp /opt/rancher/etc        # 放 containerd 的 registries.yaml
cd /opt/rancher

我们需要先准备好 registries.yaml,这一步非常关键

Rancher 容器内部跑的 K3s 有它自己的 containerd,在创建每个 Pod 前会去拉取沙箱镜像(rancher/mirrored-pause:3.6),这个拉取发生在 Kubernetes 的抽象之下,不受 CATTLE_SYSTEM_DEFAULT_REGISTRY 控制,只认 /etc/rancher/k3s/registries.yaml
如果不配置它,Rancher 启动过程中会大量出现下面这种报错:

FailedCreatePodSandBox … failed to pull image “rancher/mirrored-pause:3.6”: failed to resolve reference “docker.io/rancher/mirrored-pause:3.6”: failed to do request: … i/o timeout

nano /opt/rancher/etc/registries.yaml
mirrors:
  "docker.io":
    endpoint:
      - "https://registry.example.com"

然后编写 docker-compose.yml

nano docker-compose.yml

因为 Rancher 的 80/443 端口这里只会被 aliyun-server 上的 Caddy 通过 Tailscale 回源访问,所以端口具体使用什么值都可以。这里为了避免之后在这台机器上部署其他服务时冲突,我把它显式绑定到 8080/8443

version: "3.8"

services:
  rancher:
    image: rancher/rancher:v2.14.0
    container_name: rancher
    restart: unless-stopped
    privileged: true
    ports:
      - "8080:80"
      - "8443:443"
    volumes:
      - /opt/rancher/data:/var/lib/rancher
      - /opt/rancher/auditlog:/var/log/auditlog
      # 把 registries.yaml 挂到 K3s 默认读取的路径,让内嵌 containerd
      # 把 docker.io 的拉取(含 pause 沙箱镜像)转到私有仓库
      - /opt/rancher/etc/registries.yaml:/etc/rancher/k3s/registries.yaml:ro
    environment:
      AUDIT_LEVEL: "1"
      # 让 Rancher 所有内嵌 K3s 里跑的业务组件(Fleet、rancher-webhook、shell 等)
      # 从私有仓库拉取镜像,不再尝试走 Docker Hub;注意不要带 http:// / https://
      CATTLE_SYSTEM_DEFAULT_REGISTRY: registry.example.com
      # 使用容器内自带的 Helm 系统 Chart(monitoring / logging 等),
      # 不去 GitHub 上拉取——这对国内环境几乎是必须的
      CATTLE_SYSTEM_CATALOG: bundled

参数说明:

  • privileged: true:Rancher 容器内部会起一个本地的 Kubernetes 集群,所以必须以特权模式运行。
  • volumes 里的前两条挂载:持久化 Rancher 的核心数据(含内嵌 K3s 的 etcd)和 API 审计日志,AUDIT_LEVEL=1 打开审计。
  • volumes 的第三条是本节开头提到的 registries.yaml,作用于内嵌 K3s 的 containerd,解决 sandbox/pause 这类基础镜像也必须走私有仓库的问题。
  • CATTLE_SYSTEM_DEFAULT_REGISTRY / CATTLE_SYSTEM_CATALOG 的作用见代码注释,官方文档:Docker Install Commands - Air Gap
  • image 直接写 rancher/rancher:v2.14.0,不用带前缀,因为上一步已经给本机 Docker daemon 配置了 registry-mirrorsdocker pull 会透明命中 registry.example.com 的缓存。
  • 不在启动参数里指定域名(例如 --acme-domain),让 Rancher 使用自签名证书,由 Caddy 统一处理对外的 TLS。

最终的镜像拉取路径如下:

镜像类型拉取者被什么改写到私有镜像仓库
Rancher 本体容器 rancher/rancher:v2.14.0宿主机 Docker/etc/docker/daemon.jsonregistry-mirrors
Pod 沙箱 rancher/mirrored-pause:3.6内嵌 K3s 的 containerd/etc/rancher/k3s/registries.yaml(volume 挂载)
Rancher 业务组件(rancher-webhook、fleet、shell…)kubelet 按 Pod spec 拉CATTLE_SYSTEM_DEFAULT_REGISTRY 环境变量

启动服务:

docker compose up -d

启动之后可以验证下 containerd 是否真的吃到了 registries.yaml

docker exec rancher cat /var/lib/rancher/k3s/agent/etc/containerd/config.toml | grep -A 3 'docker.io'

看到 endpoint = ["https://registry.example.com"] 就说明配置到位了。

等几分钟让它完成初始化,可以用下面的命令确认容器跑起来了:

docker compose ps
docker compose logs -f

日志里看到 Rancher startup complete 字样就说明服务端已经准备就绪。

二、在 aliyun-server 上配置 Caddy 反向代理

由于 rancher 本身没有公网 IP,我们将它自己生成的自签名证书直接交给跑在 aliyun-server 上的 Caddy 处理,由 Caddy 统一对外签发 Let’s Encrypt 的真实证书。

确保 aliyun-server 已经通过 Tailscale 加入了同一张私网(10.64.0.11),并且能够解析到 rancher.ts.net

# 在 aliyun-server 上
tailscale status | grep rancher
ping -c 2 rancher.ts.net

aliyun-server/etc/caddy/Caddyfile 中加入以下配置:

sudo nano /etc/caddy/Caddyfile
rancher.example.com {
    # 通过 Tailscale 私网回源到 rancher
    # 这里一定要使用 HTTPS
    reverse_proxy https://rancher.ts.net:8443 {
        transport http {
            tls
            tls_insecure_skip_verify
        }
        header_up Host {host}
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto {scheme}
    }
}

保存后重载 Caddy:

sudo systemctl reload caddy

rancher.example.com 的公网 A 记录指向 aliyun-server 的公网 IP,访问 https://rancher.example.com 应该就能看到 Rancher 的欢迎页了。

三、首次登录并进行设置

1、设置管理员密码

首次访问时,Rancher 会要求你输入初始密码,直接去容器内取:

docker compose -f /opt/rancher/docker-compose.yml logs 2>&1 | grep "Bootstrap Password:"

如果看不到密码的话,也可以直接去 Secrets 里找:

docker exec -it rancher kubectl get secret --namespace cattle-system bootstrap-secret -o go-template='{{.data.bootstrapPassword|base64decode}}{{ "\n" }}'

把这个密码填入页面,按指引设置好新的管理员密码,然后登录。

2、将 server-url 固定为公网域名

这一步是整个部署最容易入坑的地方。
Rancher 的 server-url 决定了:

  • 下游集群的 cattle-cluster-agent 要连回来的地址;
  • 登录成功后前端会跳转的地址。

如果这个值填错了,你从公网访问登录之后,会被 302 到内网的 Tailscale 域名导致直接空白或超时。

登录后第一次进入界面时,Rancher 会弹窗询问 Server URL
在这里直接填公网域名:

https://rancher.example.com

如果错过了这个弹窗,可以在 > Global Settings > Settings > server-url 中修改。

为什么一定要填公网域名,而不是 Tailscale 域名?
因为我只有一个出口,而从公网访问 Rancher 时一定处于无法解析 ts.net 域名的环境下。
如果把 server-url 设成 rancher.ts.net,公网登录后 Rancher 就会把我重定向到 Tailscale 域名而变成空白页。
反过来,我们完全可以通过在 K3s 节点上配置 /etc/hosts 或 Tailscale MagicDNS,让 K3s 集群把 rancher.example.com 解析到 Tailscale IP,这样两边访问同一个域名,但走不同的链路,就不会有跳转问题了。

3、将 agent-tls-mode 调整为 System Store

从 Rancher v2.9 起,agent-tls-mode 的默认值变成了 strict,即 cattle-cluster-agent 只信任 Rancher 内置 CA 签的证书。
因为我们这里的 TLS 是由前面 Caddy 的 Let’s Encrypt 证书托管的(对 Rancher 而言属于未知的公开 CA),所以需要把这个设置改成 system-store,让 agent 信任系统根证书库中的所有 CA:

进入 > Global Settings > Settings,找到 agent-tls-mode,点击 > Edit Setting,将值改为 System Store
agent-tls-mode

官方说明:TLS Settings
如果不调这个参数,之后你导入的 K3s 集群里 cattle-cluster-agent Pod 会一直 CrashLoopBackOff 并抛 x509: certificate signed by unknown authorityunable to read CA file

四、通过 Tailscale 私网将 K3s 集群注册到 Rancher

到这里 Rancher 本身已经部署完成,剩下的就是把那台跑在另一处的 K3s v1.34.6+k3s1 集群注册进来。

1、在 Headscale 中下发自定义 DNS 记录

前面提到过,为了让 server-url 在两种访问路径下都保持一致,需要让 K3s 节点把 rancher.example.com 解析到 aliyun-server 的 Tailscale IP 而不是公网 IP,这样下游流量才会从 Tailscale 私网先到 Caddy,再由 Caddy 回源到 rancher

你可能会想:那是不是让 DNS 直接解析到 rancher 的 Tailscale IP 更简单?
可以但是我不建议,如果跳过 Caddy 直连 Rancher 容器的 8443,K3s 的 cattle-cluster-agent 会拿到 Rancher 的自签名证书,除非你把 Rancher CA 手动灌进去,否则 TLS 链路就断了。
统一走 Caddy 才能继续复用 Let’s Encrypt 的真实证书,整体更干净。

由于我的 Tailscale 是通过自建的 Headscale 管理的,可以直接利用 Headscaledns.extra_records 特性在服务端一次性下发自定义 DNS 记录,所有已经加入 Headscale 网络的客户端都会自动拿到这条记录,完全不需要在每台 K3s 节点上改 /etc/hosts

先在 aliyun-server 上拿到它自己的 Tailscale IP:

tailscale ip -4

登录到 Headscale 服务端所在的主机,修改配置文件:

sudo nano /etc/headscale/config.yaml

在已有的 dns: 小节里加入 extra_records

dns:
  magic_dns: true
  base_domain: ts.net
  override_local_dns: true  # 必须为 true,否则客户端会优先用本机 DNS 导致下面的记录被覆盖
  nameservers:
    global:
      - 223.5.5.5
      - 119.29.29.29
      - 1.1.1.1
      - 2400:3200::1
  # 下面是新增的
  extra_records:
    # rancher.example.com -> aliyun-server(跑 Caddy 的那台)
    - name: "rancher.example.com"
      type: "A"
      value: "10.64.0.11"

extra_records 里的值会被 Headscale 通过控制面协议推送给所有已连接的客户端,由 Tailscale 客户端内置的 MagicDNS 进行应答,效果等价于给所有节点批量改 /etc/hosts,但由服务端集中管理
需要注意的是 extra_records 仅支持 A / AAAA,目前还不能直接写成 rancher.ts.net 这样的 MagicDNS 域名。
相关 Issue 和官方文档:juanfont/headscale#2508 [Feature] Extra DNS Records / API support CNAMEDNS - Headscale

保存后重启 Headscale

sudo systemctl restart headscale
sudo systemctl status headscale

2、在 K3s 节点上验证 DNS 记录已下发

登录到任意一台 K3s 节点,确认这条记录已经生效:

# 刷新 Tailscale 客户端的 netmap(一般会自动同步)
sudo tailscale set --accept-dns=true
# 解析测试
getent hosts rancher.example.com
ping -c 2 rancher.example.com
curl -I https://rancher.example.com

只要解析出来的 IP 是 aliyun-server 的 Tailscale IP(10.64.0.11),就说明路径正确。

如果解析不到或仍然是公网 IP,依次检查:

  • Headscaleoverride_local_dns: true 是否已生效、magic_dns: true 是否开启;
  • 客户端加入网络时是否带了 --accept-dns=true(我在 Headscale 那篇文章里提过这个参数);
  • resolvectl status 查看是否把 100.100.100.100(Tailscale 的内置 DNS)列为了 Current DNS Server。

如果你使用的是官方的 Tailscale,没有 Headscale 的控制面,这里就需要改用 Tailscale 管理后台的 Split DNS / Global nameservers 来下发一个 override 记录,或者回退到在每个节点的 /etc/hosts 里手动写一条 10.64.0.11 rancher.example.com

3、在 Rancher 中创建导入集群

回到 Rancher UI,点击左上角 > Cluster Management,然后点击右上角的 Import Existing
开始导入

选择 Generic,填写集群名字,其他保持默认,点击 Create
创建

几秒后 Rancher 会生成两条注册命令:

# 标准方式(需要 server 证书是被系统信任的 CA 签的)
kubectl apply -f https://rancher.example.com/v3/import/xxxxxxxxxxxxxxxxxxxx.yaml

# 插入 insecure 的兜底方式
curl --insecure -sfL https://rancher.example.com/v3/import/xxxxxxxxxxxxxxxxxxxx.yaml | kubectl apply -f -

由于我们用的是 Let’s Encrypt 的真实证书,且已经把 agent-tls-mode 改成了 System Store直接使用第一条 kubectl apply 命令即可

4、在 K3s 集群中执行导入命令

SSH 到 k3s-server 节点,执行上面那条命令:

kubectl apply -f https://rancher.example.com/v3/import/xxxxxxxxxxxxxxxxxxxx.yaml

执行后过十几秒,检查 cattle-system 命名空间下的 Pod 是否都正常运行:

kubectl get pods -n cattle-system -o wide

正常情况下应该看到 cattle-cluster-agent-xxxxx 处于 Running 状态。

由于我们在第二步已经把 CATTLE_SYSTEM_DEFAULT_REGISTRY 设置成了 registry.example.com,Rancher 生成的这份导入 YAML 里 rancher/rancher-agent 的镜像地址就会自动变成 registry.example.com/rancher/rancher-agent:...,K3s 节点不需要额外配置镜像加速就能直接拉下来。

想实时看镜像拉取进度(rancher-agent 压缩后约 300 MB,第一次拉通常要几分钟)可以在 k3s-server 上手动触发一次 k3s ctr images pull

sudo k3s ctr images pull registry.example.com/rancher/rancher-agent:v2.14.0

它直连 K3s 自带的 containerd,和 kubelet 共用同一个 socket、自动去重,不会重复下载也不会干扰正在进行的拉取:

registry.example.com/rancher/rancher-agent:v2.14.0: resolved |+++++++++++++++++|
manifest-sha256:abcdef… |++++++++++++ | done
config-sha256:123456… |++++++++++++++++ | done
layer-sha256:aaa111… : downloading [====> ] 42.5 MiB/290.1 MiB
layer-sha256:bbb222… : downloading [================> ] 128.0 MiB/180.3 MiB
elapsed: 1m23s total: 170.5 M (2.1 MiB/s)

如果卡在 CrashLoopBackOff,用下面的命令看日志:

kubectl logs -n cattle-system deploy/cattle-cluster-agent

回到 Rancher UI,几十秒后集群状态就会从 Pending 变成 Active,这时就能在 Rancher 里直接管理这套远端的 K3s 集群了:
K3s 集群注册成功

结束。


Edit page