本文仅代表个人视角对懒猫 Docker 的拆解分析,内容为基于现象的倒推推测,不代表懒猫官方实现方式。

拿到任何 NAS 的第一件事是开启 SSH 功能,第二步就是用 Docker 启动容器。

懒猫微服里的 Docker 环境不太一样,一台机器上有三套:

docker:运行系统组件

pg-docker:普通的 Docker 环境,供用户日常使用

lzc-docker:运行懒猫商店应用

三套 Docker 引擎初探

先看看这三套 Docker 环境分别跑了什么,从 ps 看起:

docker ps
1
2
3
4
5
6
7
8
9
10
11
CONTAINER ID   IMAGE                                                                              COMMAND                  CREATED      STATUS                PORTS     NAMES
1838d4f379e5 registry.lazycat.cloud/lzc/lzcsys:latest "/sspk/bin/pd-service" 9 days ago Up 9 days lzc-runtime-peripheral-device-1
c2f6d791181b registry.lazycat.cloud/lzc/lzcsys:latest "/sspk/bin/lzc-ingre…" 9 days ago Up 9 days (healthy) lzc-runtime-ingress-control-1
9699c428d2b0 registry.lazycat.cloud/dexidp/dex:v2.42.0-alpine "/usr/local/bin/dock…" 9 days ago Up 9 days lzc-runtime-dex
57952c3e4ba5 registry.lazycat.cloud/lzc/lzcsys:latest "/sspk/bin/lzc-apise…" 9 days ago Up 9 days (healthy) lzc-runtime-api-servers-1
cde0eba62fd2 registry.lazycat.cloud/lzc/lzcsys:latest "/sspk/bin/lzc-pkgm" 9 days ago Up 9 days (healthy) lzc-runtime-pkgm-1
8e9c780c012c registry.corp.lazycat.cloud/homecloud/lzc-registry-proxy:v0.0.0-2887-gd16c7f25.m "/bin/sh -c /lzc-reg…" 9 days ago Up 9 days 80/tcp lzc-registry-proxy
59d3803ef304 registry.corp.lazycat.cloud/homecloud/lzc-installer:v0.0.0-2887-gd16c7f25.m "/docker-entrypoint.…" 9 days ago Up 9 days lzc-installer
c7192a7fd471 registry.corp.lazycat.cloud/homecloud/lzc-hal:v0.0.0-2887-gd16c7f25.m "/bin/sh -c /sspk/bi…" 9 days ago Up 9 days lzc-hal
1d194e975117 registry.corp.lazycat.cloud/homecloud/lzc-recovery:v0.0.0-2887-gd16c7f25.m "/docker-entrypoint.…" 9 days ago Up 9 days lzc-recovery
8338ce6a5c17 registry.corp.lazycat.cloud/homecloud/lzc-recovery:v0.0.0-2887-gd16c7f25.m "/sspk/bin/entrypoin…" 9 days ago Up 9 days

这里可以看到,系统级组件都跑在默认的 docker 下。

pg-docker ps
1
2
3
CONTAINER ID   IMAGE                                                                     COMMAND                  CREATED      STATUS        PORTS                                     NAMES
d0ae10b8fc8f registry.lazycat.cloud/u04123229/qilinzhu/ql-play:fbf2e99a00ef9a7f "sh /app/start.sh" 3 days ago Up 26 hours ql-play
0cb9ec655c16 registry.lazycat.cloud/u04123229/cloudsmithy/shuangpin:2a8ede2b23c38be8 "/docker-entrypoint.…" 6 days ago Up 6 days 0.0.0.0:5004->80/tcp, [::]:5004->80/tcp unruffled_lichterman

pg-docker 实际上就是日常部署、测试容器最常用的那套运行时环境,Dockge 默认连接的也是这里。它为了和系统 Docker 区分而改了名字,Playground 就是随便玩的意思。

image-20250520102656467

lzc-docker ps
1
2
3
4
lzc-docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
80c88ae6aa8b registry.lazycat.cloud/app-tv-controller:1.0 "/lzcinit/cloud.lazy…" 16 hours ago Up 16 hours (healthy) cloudlazycatapplzctvcontroller-app-1
fdb2211b210e registry.lazycat.cloud/lzc/tvos-release:v0.1.219 "/home/tvos/run.sh" 16 hours ago Up 16 hours 5500/tcp cloudlazycatapplzctvcontroller-tvos-1

这个是懒猫商店的 Docker 环境。实测在客户端中停止应用时,会删除对应的容器;无论执行 docker ps -a | grep auth,还是通过可视化工具查看,结果都一样。这也符合容器的使用习惯:不需要时删除,使用时重新启动,但持久化数据仍然保留。

版本和运行时对比

我们再来看一下版本,都还是一样的。所以这个就很有意思了。

1
2
3
4
5
6
lzcbox-029c588e ~ # docker --version
Docker version 27.5.1, build 9f9e405
lzcbox-029c588e ~ # pg-docker --version
Docker version 27.5.1, build 9f9e405
lzcbox-029c588e ~ # lzc-docker --version
Docker version 27.5.1, build 9f9e405

再看看存储后端,有没有什么特殊改动?可以看出后端都是 containerd。

1
2
3
4
5
6
7
8
9
lzcbox-029c588e ~ # docker info | grep -i 'Runtimes\|Default Runtime'
Runtimes: io.containerd.runc.v2 runc
Default Runtime: runc
lzcbox-029c588e ~ # pg-docker info | grep -i 'Runtimes\|Default Runtime'
Runtimes: io.containerd.runc.v2 runc
Default Runtime: runc
lzcbox-029c588e ~ # lzc-docker info | grep -i 'Runtimes\|Default Runtime'
Runtimes: io.containerd.runc.v2 runc
Default Runtime: runc

甚至连 containerd 的版本都一样。

1
2
3
4
5
6
7
8
9
10
# docker info | grep -i "containerd"
Runtimes: io.containerd.runc.v2 runc
containerd version: bcc810d6b9066471b0b6fa75f557a15a1cbf31bb
lzcbox-029c588e ~ # pg-docker info | grep -i "containerd"
Runtimes: io.containerd.runc.v2 runc
containerd version: bcc810d6b9066471b0b6fa75f557a15a1cbf31bb
lzcbox-029c588e ~ # lzc-docker info | grep -i "containerd"
Runtimes: io.containerd.runc.v2 runc
containerd version: bcc810d6b9066471b0b6fa75f557a15a1cbf31bb
lzcbox-029c588e ~ #

一开始以为这里做了特殊修改,又看了下我的 Mac 上 OrbStack 的配置,好像也没什么差别。

1
2
3
4
5
6
7
8
9
10
11
docker info | grep -i 'Runtimes\|Default Runtime'
Runtimes: io.containerd.runc.v2 runc
Default Runtime: runc


docker info | grep -i "containerd"


Runtimes: io.containerd.runc.v2 runc
containerd version: 06b99ca80cdbfbc6cc8bd567021738c9af2b36ce

多引擎共存的实现方式

DOCKER_HOST 的封装

既然三个 Docker 客户端都出奇地一致,难道是通过类似命名空间的方式隔离吗?

1
2
3
4
5
6
7
8
9
10
11
12
lzcbox-029c588e ~ # which docker
/usr/bin/docker
lzcbox-029c588e ~ # which pg-docker
/lzcsys/bin/pg-docker
lzcbox-029c588e ~ # which lzc-docker
/lzcsys/bin/lzc-docker
lzcbox-029c588e ~ # file $(which docker)
/usr/bin/docker: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=4a41bb12cfd0c306a6ede40f41cfc107b2045371, for GNU/Linux 3.2.0, with debug_info, not stripped
lzcbox-029c588e ~ # file $(which pg-docker)
/lzcsys/bin/pg-docker: Bourne-Again shell script, ASCII text executable
lzcbox-029c588e ~ # file $(which lzc-docker)
/lzcsys/bin/lzc-docker: Bourne-Again shell script, ASCII text executable

这就能发现问题了:docker 是原本的 Docker 客户端,而 pg-docker 和 lzc-docker 是封装脚本。来看一下:

所以可以得出一个关键点:懒猫并不是运行了三套完全独立的 Docker 服务,而是通过 shell 脚本封装,复用同一个 docker 客户端,切换不同的 socket 实现了“环境隔离”。这个脚本的作用相当于把 pg-docker 当成 docker 命令使用,还自动附带了环境变量 DOCKER_HOST=...。

1
2
3
4
5
6
lzcbox-029c588e ~ # cat $(which pg-docker)
#!/bin/bash
set -e

export DOCKER_HOST=unix:///data/playground/docker.sock
exec docker "$@"

这设置了 DOCKER_HOST 环境变量,使得之后执行的 docker 命令会连接到 /data/playground/docker.sock 这个 Unix Socket,而**不是默认的 /var/run/docker.sock**。

exec 是一个 shell 内建命令,它会用新的进程替换当前脚本的进程。

"$@" 表示把脚本接收到的所有参数(比如 pg-docker ps -a)原样传递给 docker 命令。

之前想上架一个 Docker 可视化工具,但一直不知道需要映射哪个 docker.sock。这下全都清楚了:有了这个 Socket,就能在容器里使用宿主机的 Docker API。

daemon.json 配置详解

与之对应的还有 daemon.json。除了用来修改代理之外,我们还能配置这些内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
lzcbox-029c588e /data/playground/data # cat /lzcsys/var/playground/daemon.json
{
"bridge": "",
"containerd-namespace": "playground-docker",
"containerd-plugins-namespace": "playground-docker",
"data-root": "/data/playground/data/docker",
"default-address-pools": [

],
"exec-root": "/data/playground/docker",
"hosts": [
"unix:///data/playground/docker.sock"
],
"insecure-registries": [
"registry.lazycat.cloud"
],
"pidfile": "/data/playground/docker.pid"

这样多个 Docker 环境就能共存了,例如:

  • 系统默认的 /var/run/docker.sock
  • 一个沙箱环境 /data/playground/docker.sock

这么设置好之后可以快速切换上下文,而不用每次都手动设置 DOCKER_HOST。

在我开发的容器可视化面板中,已经可以指定 Docker Socket 作为连接地址,参考:

1
2
3
4
5
6
7
8
services:
containly:
image: registry.lazycat.cloud/u04123229/cloudsmithy/containly:30e4e3279afe9a52
ports:
- 5003:5000
volumes:
- /data/playground/docker.sock:/var/run/docker.sock
restart: unless-stopped

image-20250520104141112

三套 daemon.json 对比分析

从实际查找结果来看,懒猫为三套 Docker 引擎配置了不同的 daemon.json 文件和运行时环境:

  • 系统组件专用(docker):/etc/docker/daemon.json
  • 用户 playground 环境(pg-docker):/lzcsys/var/playground/daemon.json
  • 懒猫商店环境(lzc-docker):/lzcsys/etc/docker/daemon.json
1
2
3
4
5
6
7
8
9
sudo find / -type f -name daemon.json 2>/dev/null

/etc/docker/daemon.json
/run/lzcsys/boot/lzc-os-init/var/playground/daemon.json
/run/lzcsys/boot/lzc-os-overlay/lowerdir/lzcsys/etc/docker/daemon.json
/run/lzcsys/boot/lzc-os-overlay/lowerdir/lzcsys/lzcsys/etc/docker/daemon.json
/lzcsys/etc/docker/daemon.json
/lzcsys/var/playground/daemon.json

每个配置文件中都指定了独立的:

  • data-root
  • exec-root
  • pidfile
  • hosts(即 sock 文件路径)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 默认的 Docker 引擎
lzcbox-029c588e ~ # cat /etc/docker/daemon.json
{
"registry-mirrors": [
],
"insecure-registries": [
"registry.lazycat.cloud"
],
"log-driver": "journald",
"cgroup-parent": "sys_docker.slice"
}

# 商店的 Docker 引擎
lzcbox-029c588e ~ # cat /lzcsys/etc/docker/daemon.json
{
"bridge": "none",
"insecure-registries": [
"registry.lazycat.cloud"
],
"default-address-pools": [
],
"ipv6": true,
"hosts": [
"unix:///lzcsys/run/lzc-docker/docker.sock"
],
"containerd-namespace": "lzc-docker",
"containerd-plugins-namespace": "lzc-docker-plugins",
"exec-root": "/lzcsys/run/lzc-docker/docker",
"pidfile": "/lzcsys/run/lzc-docker/docker.pid",
"data-root": "/lzcsys/run/data/system/docker",
"cgroup-parent": "lzc_docker.slice"
}

# Playground 的 Docker 引擎
cat /lzcsys/var/playground/daemon.json
{
"bridge": "",
"containerd-namespace": "playground-docker",
"containerd-plugins-namespace": "playground-docker",
"data-root": "/data/playground/data/docker",
"default-address-pools": [],
"exec-root": "/data/playground/docker",
"hosts": [
"unix:///data/playground/docker.sock"
],
"insecure-registries": [
"registry.lazycat.cloud"
],
"pidfile": "/data/playground/docker.pid"

小结

懒猫微服总会给我惊喜。除了极客风格的外壳和性能突出的硬件,里面的软件设计也同样有意思,这套设计让我对 Docker 有了更深的认识。

三套 Docker 环境共存不是表面上的改名,而是通过 containerd 的 namespace 配合脚本封装,在容器之上再抽象一层运行时,把 Playground、系统和商店隔离成三界,却又共用一套内核,很好玩。


忘机山人二维码

扫码关注「忘机山人」