Docker 基础体系 · 第 24/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。
Docker Context 与远程 Daemon:SSH、TLS、权限和环境隔离
Docker 命令通常被理解为“在本机运行容器”,但更准确的模型是:
docker CLI
│ Docker Engine API
▼
Docker Daemon(dockerd)
│
├── 镜像、容器、网络、卷
├── BuildKit 构建
└── Linux 容器运行时
docker 命令本身主要是客户端。真正创建容器、拉取镜像、挂载卷、执行构建和修改网络状态的是 Docker Daemon,通常称为 dockerd。Docker Context 是 CLI 用来选择“连接哪个 Daemon,以及如何连接”的配置对象;SSH 和 TLS 则是两种不同的远程连接安全机制。
这一区分决定了许多容易误判的行为:
- 连接的是远程 Daemon,容器和镜像就位于远程主机,而不是当前终端所在的机器。
-v "$PWD":/app中的$PWD首先由客户端 shell 展开,但真正尝试挂载该路径的是 Daemon 所在主机。docker build发送构建上下文给远程构建端,构建结果通常进入远程 Daemon 的镜像存储。docker context不会复制镜像、容器、卷或/var/lib/docker,它只改变客户端的连接目标。
1. Docker Context 解决的是什么问题
1.1 没有 Context 时:连接目标由环境变量或默认值决定
Docker CLI 需要一个 Engine API endpoint。常见 endpoint 包括:
unix:///var/run/docker.sock
ssh://deploy@example.com
tcp://docker.example.com:2376
其中:
unix://通常表示本机 Unix socket;ssh://表示通过 SSH 连接远程主机;tcp://表示通过 TCP 连接 Docker Engine API,生产环境通常还要配合 TLS。
在最简单的本地安装中,CLI 默认连接:
unix:///var/run/docker.sock
可以用环境变量临时改变连接目标:
export DOCKER_HOST=ssh://deploy@example.com
docker ps
此时 docker ps 查询的是远程主机上的容器。
这种方式的问题是连接目标隐藏在 shell 环境中:
echo "$DOCKER_HOST"
docker context ls
脚本、IDE、CI 任务和不同终端可能拥有不同的环境变量,导致同一条命令实际连接不同的 Daemon。
1.2 Context 是带名称的连接配置
Context 为连接配置分配一个名称:
docker context ls
典型输出类似:
NAME DESCRIPTION DOCKER ENDPOINT
default * Current DOCKER_HOST based configuration unix:///var/run/docker.sock
创建一个 SSH Context:
docker context create prod-ssh \
--description "Production Engine over SSH" \
--docker "host=ssh://deploy@docker-prod.example.com"
切换当前 Context:
docker context use prod-ssh
docker info
docker ps
docker context inspect 可查看底层配置:
docker context inspect prod-ssh
其结果包含类似信息:
[
{
"Name": "prod-ssh",
"Metadata": {},
"Endpoints": {
"docker": {
"Host": "ssh://deploy@docker-prod.example.com",
"SkipTLSVerify": false
}
}
}
]
Context 本质上保存了:
- endpoint 类型和地址;
- TLS CA、客户端证书和私钥等连接材料的位置或内容;
- Context 名称、描述和元数据。
它不保存:
- 容器列表;
- 镜像层;
- 容器卷;
- Daemon 的
data-root; - Daemon 配置文件;
- 远程主机的 shell 环境。
因此,删除 Context 不会删除远程容器:
docker context rm prod-ssh
这只删除本机 CLI 的连接配置,不会向远程 Daemon 发送容器删除操作。
1.3 Context 的选择优先级与验证方法
实际执行命令前,必须确认当前连接目标:
docker context show
docker context inspect "$(docker context show)"
docker info --format '{{.Name}} {{.DockerRootDir}}'
docker context use NAME 修改当前用户的 Docker CLI 配置。对单条命令,也可以使用全局选项:
docker --context prod-ssh ps
常见环境变量包括:
DOCKER_HOST
DOCKER_CONTEXT
工程上最重要的规则是:不要只看 docker context ls,还要检查是否有环境变量覆盖了预期配置。
例如:
env | grep '^DOCKER_'
docker context show
docker info --format 'Server={{.Name}} Root={{.DockerRootDir}}'
DOCKER_CONTEXT 用于指定 Context 名称;DOCKER_HOST 用于直接指定 endpoint。不同 Docker CLI 版本和命令组合中,环境变量与显式参数可能造成覆盖关系,因此对高风险操作,使用显式的:
docker --context prod-ssh rm -f container_name
比依赖当前 shell 状态更容易审计。
1.4 default Context 的特殊性
default Context 通常代表传统的本地连接方式:
unix:///var/run/docker.sock
但它不是一个独立的远程 Docker 环境,也不是“默认的镜像仓库”。切换 Context 不会在 default 和其他 Context 之间共享容器、镜像或卷。
例如:
docker context use default
docker images
和:
docker context use prod-ssh
docker images
可能得到完全不同的镜像列表。这是因为两个命令访问了两个不同的 Daemon 存储。
2. 远程 Daemon 的数据流:谁执行什么
远程操作可以抽象为:
sequenceDiagram
participant C as Docker CLI
participant T as SSH/TLS 传输
participant D as 远程 dockerd
participant R as 远程容器运行时/存储
C->>T: 请求 Engine API
T->>D: 转发 HTTP API 请求
D->>R: 创建容器、拉取镜像或执行构建
R-->>D: 返回状态
D-->>T: API 响应
T-->>C: 命令结果
例如执行:
docker --context prod-ssh run --rm alpine uname -a
发生的事情是:
- 本地 CLI 根据
prod-ssh找到 SSH endpoint; - CLI 通过 SSH 建立到远程主机的连接;
- 远程主机上的 Docker Daemon 接收
POST /containers/create等 API 请求; - 远程 Daemon 在自己的 Linux 内核、文件系统、网络和存储中创建 Alpine 容器;
uname -a看到的是远程主机内核,而不是本地客户端的内核。
这解释了一个关键边界:
Docker 容器使用的是 Daemon 所在主机的容器运行环境;Docker CLI 所在主机只是 API 客户端。
2.1 Bind mount 路径属于 Daemon 主机
在本地连接时:
docker run --rm -v "$PWD":/src alpine ls /src
shell 将 $PWD 展开为本机路径,例如:
/home/alice/project
如果使用远程 Context:
docker --context prod-ssh run --rm \
-v "$PWD":/src alpine ls /src
远程 Daemon 会尝试在远程主机上挂载:
/home/alice/project
它不会自动读取客户端机器上的同名目录。因此常见结果是:
- 远程路径不存在;
- 挂载了远程主机上另一个同名目录;
- Daemon 因权限不足拒绝创建容器;
- 在某些配置下创建空目录后产生更隐蔽的错误。
验证方法:
docker --context prod-ssh run --rm \
-v /tmp:/mnt alpine sh -c 'hostname; ls -la /mnt'
这里 /tmp 必须是远程主机上的 /tmp。
如果构建上下文来自本地:
docker --context prod-ssh build -t demo:latest .
则 . 由客户端打包并发送给远程构建端;这与 bind mount 不同。构建上下文是 API 请求中的数据流,而 bind mount 是 Daemon 创建容器时访问的主机路径。
3. SSH 连接:借用 SSH 身份验证和传输保护
3.1 SSH Context 的工作机制
SSH Context 的典型形式是:
ssh://user@host
ssh://user@host:port
创建示例:
docker context create staging-ssh \
--docker "host=ssh://docker-admin@staging.example.com"
执行:
docker --context staging-ssh info
Docker CLI 通常通过 SSH 与远程主机上的 Docker CLI 建立 API 转发通道。远端用户需要能够访问 Docker Daemon 的 Unix socket,常见路径为:
/var/run/docker.sock
SSH 连接成功并不等于 Docker API 权限足够。整个链路有两个独立的认证条件:
SSH 用户认证成功
AND
该用户有权访问远程 Docker socket
可以手工验证第二个条件:
ssh docker-admin@staging.example.com docker version
如果 SSH 登录成功,但远端执行 Docker 命令得到:
permission denied while trying to connect to the Docker daemon socket
那么问题不是 Context 地址,而是远端用户的 socket 权限。
3.2 SSH 前置条件
客户端需要能够正常使用目标 SSH 主机:
ssh -v docker-admin@staging.example.com
建议先确认:
- DNS 或
/etc/hosts能解析目标; - SSH 端口可达;
- 主机密钥已经经过验证;
- 客户端私钥权限正确;
~/.ssh/config中的别名、端口和IdentityFile生效;- 远端安装了 Docker CLI 或可用的 Docker API 转发命令;
- 远端用户能够访问 Docker socket。
例如配置:
Host staging-docker
HostName staging.example.com
User docker-admin
Port 2222
IdentityFile ~/.ssh/staging_ed25519
IdentitiesOnly yes
然后创建 Context:
docker context create staging-ssh \
--docker "host=ssh://staging-docker"
使用 SSH 别名的好处是将端口、密钥和跳板配置留在 SSH 配置中,而不是散落在脚本中。
3.3 SSH 的安全边界
SSH 提供:
- 主机身份验证;
- 用户身份验证;
- 加密传输;
- 可使用跳板机、代理和短期凭证;
- 可纳入现有 SSH 审计体系。
但 SSH 不会自动提供 Docker 级别的细粒度授权。如果远端用户属于 docker 组,通常就可以控制整个 Docker Daemon。
因此:
sudo usermod -aG docker docker-admin
虽然常用于解决 socket 权限问题,但它具有高风险。Docker Daemon 通常具备 root 级能力,能够:
- 挂载宿主机文件系统;
- 创建特权容器;
- 使用宿主机设备;
- 修改网络和存储;
- 通过容器逃逸漏洞扩大影响范围。
“只允许这个用户启动几个容器”不是 Docker socket 权限本身能够表达的策略。若需要细粒度控制,应在 Daemon 前增加经过认证和授权的代理,或使用独立的受限执行系统,而不是直接把 /var/run/docker.sock 授给更多用户。
3.4 Rootful 与 Rootless Daemon
Rootful Docker 通常使用:
/var/run/docker.sock
Rootless Docker 的 socket 通常位于用户运行时目录,例如:
/run/user/1000/docker.sock
具体路径应从远程主机查询:
ssh user@host 'docker context show; docker info --format "{{json .}}"'
如果远程用户使用 rootless Docker,SSH 登录用户必须是启动该 Rootless Daemon 的用户;使用另一个用户登录,通常看不到同一个用户级 socket。
Rootless 会降低 Daemon 和容器对宿主机的权限,但也可能带来边界差异,例如:
- 可使用的网络模式不同;
- 某些设备和挂载能力受限;
- 宿主机目录权限仍按实际用户检查;
- rootless Daemon 的镜像和容器存储与 rootful Daemon 完全分离。
“同一台主机”不代表“同一个 Docker 环境”。rootful 和 rootless 可能是两个独立的 Daemon。
4. TLS 远程连接:直接保护 Docker Engine API
4.1 为什么不能裸露 TCP API
Docker Engine API 是高权限控制接口。若 Daemon 监听:
tcp://0.0.0.0:2375
且没有 TLS,任何能访问该端口的主机都可能直接控制 Docker。典型风险包括:
curl http://docker.example.com:2375/containers/json
更危险的是攻击者可以调用创建容器、挂载宿主机和执行命令等 API。
因此,生产环境不应把未加密、未认证的 Docker API 直接暴露到不可信网络。常见安全配置是:
tcp://docker.example.com:2376
并启用双向 TLS,也就是:
- Daemon 使用服务端证书;
- CLI 验证 Daemon 的 CA 和服务端身份;
- Daemon 验证 CLI 提供的客户端证书;
- 双方都通过证书完成身份认证。
TLS 保护的是 TCP API 通道;它不会自动实现 Docker 对象级 RBAC。
4.2 TLS 连接中的证书角色
典型证书关系如下:
CA
├── 签发 dockerd 服务端证书
└── 签发 Docker CLI 客户端证书
客户端需要:
- CA 证书,用于验证 Daemon;
- 客户端证书;
- 客户端私钥。
Daemon 需要:
- 服务端证书;
- 服务端私钥;
- 用于验证客户端证书的 CA。
概念配置可能类似:
{
"hosts": [
"unix:///var/run/docker.sock",
"tcp://0.0.0.0:2376"
],
"tls": true,
"tlscacert": "/etc/docker/pki/ca.pem",
"tlscert": "/etc/docker/pki/server-cert.pem",
"tlskey": "/etc/docker/pki/server-key.pem",
"tlsverify": true
}
实际配置字段和启动参数必须与目标 Docker Engine 版本及服务管理方式一致。更改 hosts 或 TLS 相关配置前,应确认 systemd unit 是否已经通过 -H 等参数指定监听地址;同一配置项同时出现在启动参数和 daemon.json 中可能造成冲突,甚至使 Daemon 无法启动。
启动后验证:
sudo systemctl restart docker
sudo systemctl status docker --no-pager
sudo journalctl -u docker -n 100 --no-pager
ss -lntp | grep 2376
不能只检查端口是否监听。还要验证证书身份和客户端认证。
4.3 创建 TLS Context
可以创建带 TLS 参数的 Context:
docker context create prod-tls \
--docker "host=tcp://docker-prod.example.com:2376,ca=$HOME/.docker/tls/ca.pem,cert=$HOME/.docker/tls/cert.pem,key=$HOME/.docker/tls/key.pem"
然后测试:
docker --context prod-tls version
docker --context prod-tls info
成功时,docker version 通常会同时显示 Client 和 Server 信息。若只显示 Client 或报连接错误,说明 API 通道没有完成。
TLS 常见失败路径及含义:
| 错误表现 | 常见原因 |
|---|---|
connection refused |
端口未监听、防火墙拒绝、Daemon 未启动 |
context deadline exceeded |
网络不可达、代理或路由问题 |
x509: certificate signed by unknown authority |
CA 不正确或未配置 |
certificate is valid for ... |
访问地址不在服务端证书 SAN 中 |
remote error: tls: bad certificate |
客户端证书不受 Daemon 信任、证书过期或密钥不匹配 |
permission denied |
TCP/TLS 已连通,但 API 或底层主机策略拒绝请求 |
4.4 SkipTLSVerify 的真实含义
某些 Context 可以配置跳过服务端证书验证。它的作用是:
- 不严格验证服务端证书链或主机身份。
它不等于:
- 关闭加密;
- 确认对端身份;
- 修复错误的证书 SAN;
- 解决客户端证书认证失败;
- 提供更细粒度授权。
在测试环境中,跳过验证可能帮助定位“是网络问题还是证书身份问题”;在生产环境中,它会使中间人攻击更容易发生。正确做法通常是为服务端证书签发包含实际连接地址的 SAN,并向客户端配置正确 CA。
5. SSH 与 TLS 的差异
两者都可以承载 Docker Engine API,但信任模型不同。
| 维度 | SSH | TLS |
|---|---|---|
| 传输 | SSH 通道 | TLS TCP 通道 |
| 用户认证 | SSH 用户、密钥、代理等 | 客户端证书或其他 TLS 认证 |
| 主机认证 | SSH host key | CA 签发的服务端证书 |
| 网络要求 | 通常只需 SSH 端口 | 需要暴露 TLS API 端口 |
| 权限入口 | 远端 Unix socket 权限 | Daemon 的 TLS 客户端认证及底层策略 |
| 跳板支持 | SSH 原生支持较好 | 通常需网络层或代理支持 |
| 审计 | SSH 登录和命令审计 | TLS 身份和 API 日志,取决于部署 |
| 典型场景 | 运维、开发、内网访问 | 自动化平台、跨网络 Engine API |
选择 SSH 不意味着“不需要权限管理”;选择 TLS 也不意味着“有 Docker RBAC”。两者解决的主要是安全连接和身份认证,Docker Daemon 本身的高权限性质仍然存在。
6. 权限:三层身份不能混为一谈
远程 Docker 操作至少涉及三层身份:
客户端进程身份
│
├── SSH 用户或 TLS 客户端证书身份
│
远程 Docker Daemon 身份
│
└── 访问宿主机、镜像存储、网络和容器运行时的权限
此外,容器内部还有第四层身份:
容器内 UID/GID
6.1 客户端用户不等于容器用户
执行:
docker --context prod-ssh run --rm alpine id
输出可能是:
uid=0(root) gid=0(root) groups=0(root)
这里的 root 是容器内 root,不等于客户端 shell 用户,也不必然等于远程登录用户。容器内 root 的实际宿主机能力还受到:
- Daemon 是否 rootless;
- 容器是否 privileged;
- Linux capabilities;
- seccomp;
- LSM;
- user namespace;
- 挂载和设备配置
等因素影响。
6.2 Bind mount 中的 UID/GID
远程主机上的目录权限由远程内核检查。比如远程目录:
ssh deploy@host 'stat -c "%u:%g %a %n" /srv/app'
容器进程若以 UID 1000 运行:
docker --context prod-ssh run --rm \
--user 1000:1000 \
-v /srv/app:/app \
alpine sh -c 'id; touch /app/test'
只有在远程 /srv/app 允许 UID 1000 写入时,touch 才会成功。客户端本地用户的 UID 不参与这次远程挂载权限检查。
6.3 镜像仓库认证也有作用域
执行:
docker login registry.example.com
凭据通常保存于当前客户端的 Docker 配置,而不是远程 Daemon 的配置。推送时:
docker --context prod-ssh push registry.example.com/team/app:1.0
认证凭据由客户端参与 API 请求;但镜像实际存储在远程 Daemon 的镜像存储中。
另一方面,如果远程 Daemon 自己执行拉取,例如:
docker --context prod-ssh run private-registry.example.com/team/app:1.0
则需要确认凭据如何传递以及请求由谁发起。不能简单假设“本地 docker login 后,远程主机上的所有服务都自动拥有仓库权限”。应通过:
docker --context prod-ssh pull private-registry.example.com/team/app:1.0
实际验证,并区分:
- CLI 代表用户向 Daemon 发起 pull;
- Daemon、Swarm 或其他服务在远程主机自行访问仓库。
7. 环境隔离:至少区分四种环境
远程 Docker 最容易出现的问题,不是连接失败,而是命令成功执行却作用在错误环境。应区分:
- 客户端 shell 环境:执行
docker的机器; - Docker CLI 配置环境:Context、凭据、插件和环境变量;
- Daemon 主机环境:远程文件系统、内核、代理、DNS、镜像存储;
- 容器或构建环境:容器内变量、构建参数、BuildKit secret。
7.1 客户端环境变量
例如:
export DOCKER_CONTEXT=prod-ssh
export DOCKER_DEFAULT_PLATFORM=linux/amd64
这些变量首先影响客户端行为。它们不会自动成为容器内环境变量。
容器环境变量需要显式传入:
docker --context prod-ssh run --rm \
-e APP_ENV=production \
alpine sh -c 'echo "$APP_ENV"'
构建参数也必须显式声明:
FROM alpine
ARG APP_ENV=development
RUN echo "build environment: $APP_ENV"
构建命令:
docker --context prod-ssh build \
--build-arg APP_ENV=production \
-t demo:latest .
ARG 不应被当作秘密传输机制,因为它可能出现在构建元数据、历史记录或日志中。秘密应使用 BuildKit secret 机制,并验证目标 Docker/Buildx 版本和调用方式。
7.2 Daemon 环境不会继承客户端 shell
远程 Daemon 的代理、DNS、镜像源和 data-root 由远程主机上的服务配置决定。例如,客户端设置:
export HTTP_PROXY=http://proxy.local:3128
不会自动让远程 dockerd 使用该代理。远程 Daemon 需要在其 systemd 服务环境或 /etc/docker/daemon.json 等配置中单独设置。
诊断远程 Daemon 配置:
docker --context prod-ssh info
ssh deploy@docker-prod.example.com \
'systemctl show docker --property=Environment'
ssh deploy@docker-prod.example \
'cat /etc/docker/daemon.json'
docker info 可观察部分生效状态,例如:
Docker Root Dir;Server Version;- 存储驱动;
- Cgroup 驱动;
- 代理信息;
- Registry 配置。
配置文件内容不一定完整反映最终生效状态,因此应结合 systemd unit、启动参数和日志检查。
7.3 Compose 使用哪个环境
Compose 通过 Docker CLI 连接到当前选定的 Engine。先切换 Context:
docker context use staging-ssh
docker compose up -d
Compose 中的:
services:
web:
image: nginx:alpine
volumes:
- ./site:/usr/share/nginx/html:ro
当使用远程 Context 时,./site 的语义需要特别谨慎。Compose 会处理本地项目文件和构建上下文,但最终容器 bind mount 仍由远程 Daemon 依据其主机文件系统创建。不同 Compose 版本、挂载类型和实现路径可能对相对路径处理存在差异,因此不能把远程 Compose 当作“远程主机自动拥有本地工作目录”。
可靠做法是:
- 将部署文件和所需资源同步到远程主机;
- 使用远程主机存在的绝对路径;
- 或使用镜像内置资源、命名卷和配置管理系统;
- 在部署前验证路径:
ssh deploy@docker-prod.example.com 'test -d /srv/site && echo exists'
Compose 项目状态还可能依赖:
- 当前 Context;
.env文件;- shell 环境变量;
- Compose 文件所在目录;
- 远程 Daemon 上已有的网络和卷。
因此部署脚本应明确指定工作目录和 Context,而不是依赖操作者当前终端:
cd /srv/deploy/myapp
docker --context prod-ssh compose config
docker --context prod-ssh compose up -d
docker compose config 适合检查变量替换和最终配置,但它不能证明远程 bind mount 路径存在,也不能证明远程 Daemon 有拉取私有镜像的权限。
8. Context、BuildKit 与 Builder 不是同一个抽象
8.1 Docker Context 选择的是 Engine endpoint
Context 解决:
Docker CLI -> 哪个 Docker Engine API
它不等价于:
BuildKit builder -> 哪个构建服务
现代 Docker 使用 BuildKit 构建镜像。常见情况下:
docker --context prod-ssh build -t app:latest .
构建请求会通过选定的 Engine 执行,并由该环境中的 BuildKit 完成构建。但 docker buildx 可以显式使用独立 builder:
docker buildx ls
docker buildx inspect
一个 builder 可能连接到:
- 当前 Docker Engine;
- 另一个 Docker container driver;
- Kubernetes;
- 远程 BuildKit daemon。
因此,排查“镜像为什么不在预期位置”时,必须同时检查:
docker context show
docker buildx ls
docker buildx inspect --bootstrap
如果使用独立 builder,构建缓存和输出位置可能与当前 Context 的 Daemon 不同。
8.2 构建结果的输出位置
以下命令通常将镜像加载到构建所连接的 Docker 环境:
docker buildx build --load -t app:latest .
如果 builder 是远程或独立的,--load 的目标取决于 builder driver;不能简单推断为当前 shell 所在主机。
显式推送更容易定义结果位置:
docker buildx build \
--platform linux/amd64,linux/arm64 \
--push \
-t registry.example.com/team/app:1.0 \
.
这里最终结果进入镜像仓库,而不是依赖某个远程 Daemon 的本地镜像存储。多平台构建通常要求 builder 支持相应平台,并可能需要 QEMU 或原生节点;这属于构建执行环境问题,不是 Context 本身提供的能力。
9. Linux 容器边界
本文讨论的是 Docker Engine 管理 Linux 容器的典型模型。远程 Context 只改变 Engine 的连接位置,不改变 Linux 容器的基本边界:
- 容器共享 Daemon 主机的 Linux 内核;
uname -a反映 Daemon 主机内核;- 容器文件系统来自镜像层和容器可写层;
- bind mount 访问 Daemon 主机路径;
- 网络模式和网桥位于 Daemon 主机;
- 命名卷存储在 Daemon 主机;
/var/lib/docker或 rootless 对应的数据目录位于 Daemon 主机。
例如:
docker --context prod-ssh volume create app-data
docker --context prod-ssh volume inspect app-data
Mountpoint 会是远程主机上的路径,例如:
/var/lib/docker/volumes/app-data/_data
不能从客户端直接把这个路径当作本地文件访问。若要读取数据,应通过容器、远程 shell、备份工具或应用层协议完成,而不是直接操作 Docker 的内部存储目录。
直接修改:
/var/lib/docker
中的内容具有破坏性,可能破坏内容寻址存储、容器元数据和卷一致性。备份应使用应用层备份、卷快照或受支持的停止/导出流程。
10. 从零配置一个 SSH 远程 Context
假设:
- 远程主机为
docker01.example.com; - SSH 用户为
deploy; - 远程 Daemon 已运行;
deploy有权访问 Docker socket。
先验证 SSH:
ssh deploy@docker01.example.com 'docker version'
预期应能看到 Server 端版本信息。若失败,按顺序检查:
ssh -v deploy@docker01.example.com
ssh deploy@docker01.example.com 'id'
ssh deploy@docker01.example.com 'ls -l /var/run/docker.sock'
ssh deploy@docker01.example.com 'docker ps'
创建 Context:
docker context create docker01 \
--description "docker01 via SSH" \
--docker "host=ssh://deploy@docker01.example.com"
验证 Context:
docker --context docker01 info --format \
'Name={{.Name}} Root={{.DockerRootDir}} Server={{.ServerVersion}}'
然后执行一个不会留下容器的测试:
docker --context docker01 run --rm alpine \
sh -c 'echo "container host:"; hostname; echo "kernel:"; uname -a'
此时应注意:
hostname是远程创建的容器 ID 或容器主机名;uname -a使用远程主机内核;- Alpine 镜像可能需要从远程网络拉取;
- 镜像层会进入远程 Daemon 的存储。
检查镜像位置:
docker --context docker01 images alpine
docker context use default
docker images alpine
两次结果可能不同,这正是环境隔离的表现。
11. 从零诊断一个 TLS Context
假设远程服务监听:
docker01.example.com:2376
先做网络层检查:
nc -vz docker01.example.com 2376
再检查服务端证书:
openssl s_client \
-connect docker01.example.com:2376 \
-servername docker01.example.com \
-showcerts
重点观察:
- 是否完成 TLS 握手;
- 服务端证书的 SAN 是否包含访问名称;
- 证书链是否由预期 CA 签发;
- 有效期是否正确。
创建 Context:
docker context create docker01-tls \
--docker "host=tcp://docker01.example.com:2376,ca=$HOME/.docker/pki/ca.pem,cert=$HOME/.docker/pki/client-cert.pem,key=$HOME/.docker/pki/client-key.pem"
验证:
docker --context docker01-tls version
docker --context docker01-tls info
若失败,不要立即设置跳过验证。应按层次定位:
TCP 建立?
└─ 否:路由、防火墙、监听地址
TLS 握手?
└─ 否:CA、SAN、证书有效期、密钥
客户端认证?
└─ 否:client cert、Daemon 信任 CA、文件权限
Engine API 请求?
└─ 否:Daemon 配置、授权代理、API 版本
Docker 对象操作?
└─ 否:镜像权限、挂载路径、卷权限、资源限制
客户端私钥必须限制权限:
chmod 600 "$HOME/.docker/pki/client-key.pem"
不要把包含私钥的 Context 导出包上传到公共制品库或提交到 Git。Context 导出、复制和备份操作可能包含连接所需的 TLS 材料,必须按凭据处理。
12. 常见误解与反例
12.1 “Context 是远程 Docker 环境的副本”
错误。Context 只是本地 CLI 配置。
反例:
docker context create test-ssh \
--docker "host=ssh://user@host"
docker context rm test-ssh
远程主机上的容器仍然存在。只有显式执行:
docker --context test-ssh rm -f my-container
才会修改远程状态。
12.2 “远程 Context 会自动同步当前目录”
错误。命令行客户端的当前目录和远程 Daemon 的文件系统是两个不同的环境。
反例:
docker --context test-ssh run --rm \
-v "$PWD":/work alpine ls /work
这不会把本地 $PWD 上传到远程主机。若要传输文件,应使用:
tar -C . -cf - . | ssh user@host 'mkdir -p /srv/app && tar -C /srv/app -xf -'
然后使用远程路径挂载:
docker --context test-ssh run --rm \
-v /srv/app:/work alpine ls /work
生产环境还应考虑文件所有者、符号链接、特殊文件和传输过程中的一致性。
12.3 “能 SSH 登录就能使用 Docker”
错误。SSH 身份和 Docker socket 权限是两个条件。
反例:
ssh user@host true
ssh user@host docker ps
第一条成功不证明第二条成功。应单独检查:
ssh user@host 'id; stat /var/run/docker.sock; docker ps'
12.4 “TLS 证书解决了所有权限问题”
错误。TLS 主要解决连接加密和对端身份认证。拥有有效客户端证书的主体通常仍可能获得广泛的 Engine API 权限,除非前方还有授权代理或隔离的 Daemon。
12.5 “镜像在本地构建,所以远程 Context 只负责运行”
错误。命令:
docker --context test-ssh build -t demo .
会把构建请求发送给远程构建环境。构建缓存、镜像输出和拉取基础镜像的网络位置,都需要按实际 builder 和 Engine 连接关系判断。
12.6 “Compose 文件中的相对路径永远来自客户端”
不能这样概括。Compose 会在客户端解析项目配置和构建上下文,但 bind mount 最终由 Daemon 在其主机上执行。远程部署中应使用验证过的远程路径,或者避免依赖宿主机 bind mount。
13. 生产环境中的隔离和取舍
13.1 开发环境适合 SSH 的原因
SSH Context 通常适合:
- 开发者临时连接远程开发机;
- 通过跳板机访问内网 Engine;
- 不希望开放 Docker TCP 监听端口;
- 已有成熟 SSH 密钥、短期凭证和审计体系。
其主要代价是:
- 远端用户需要 Docker socket 权限;
- Docker socket 权限往往接近 root;
- 依赖 SSH 客户端和远端命令环境;
- 大量并发自动化任务未必适合通过单纯 SSH 管道承载。
13.2 自动化平台常见 TLS 取舍
TLS API 适合:
- CI/CD 或平台服务通过固定 API endpoint 调用 Engine;
- 需要证书生命周期管理;
- 网络层已经有严格分区;
- 希望避免为每个 API 请求建立 SSH 会话。
但必须配套:
- 证书签发、轮换和吊销;
- 私钥安全存储;
- 防火墙和网络访问控制;
- Daemon API 暴露面最小化;
- 审计客户端身份和请求;
- 必要时使用授权代理或每个租户独立 Daemon。
不要通过公网直接暴露 Docker API。即使启用了 TLS,错误的网络范围和过大的证书权限仍会造成高影响风险。
13.3 用独立 Daemon 实现环境隔离
如果开发、测试和生产共用同一个 Daemon,那么 Context 只能减少“连错 endpoint”的概率,不能提供真正的安全隔离。更强的隔离方式是:
dev Context -> dev Daemon
staging Context -> staging Daemon
prod Context -> prod Daemon
每个 Daemon 拥有独立的:
- 容器和镜像存储;
- 网络;
- 卷;
- 代理和镜像源配置;
- TLS 或 SSH 认证入口;
- 资源限制和审计策略。
Context 命名应能表达环境和连接方式,例如:
dev-local
staging-ssh
prod-tls
高风险命令使用显式 Context:
docker --context prod-tls compose ps
docker --context prod-tls service ls
脚本开始时应验证目标身份,而不是直接执行删除或部署:
set -euo pipefail
context="${DOCKER_CONTEXT:-default}"
docker --context "$context" info --format \
'target={{.Name}} root={{.DockerRootDir}} version={{.ServerVersion}}'
在自动化系统中,还应将 Context、证书、SSH 私钥和仓库凭据作为独立秘密管理,避免把它们混入应用源码或镜像层。
14. 一套可重复的验证顺序
当“远程 Docker 不工作”时,按以下顺序比反复修改 Context 更有效。
第一步:确认客户端实际选择
docker context show
docker context ls
env | grep '^DOCKER_'
docker context inspect "$(docker context show)"
第二步:确认传输层
SSH:
ssh -v user@host true
ssh user@host docker version
TLS:
nc -vz host 2376
openssl s_client -connect host:2376 -servername host
第三步:确认 Server API
docker --context NAME version
docker --context NAME info
version 主要确认客户端与服务端 API 通信;info 进一步提供 Daemon 的存储、运行时、资源和配置摘要。
第四步:确认远程状态位置
docker --context NAME info --format \
'name={{.Name}} root={{.DockerRootDir}} driver={{.Driver}}'
docker --context NAME ps -a
docker --context NAME images
docker --context NAME volume ls
第五步:确认路径和身份
ssh user@host 'hostname; id; pwd; ls -ld /path'
docker --context NAME run --rm \
--user 1000:1000 \
-v /path:/mnt alpine sh -c 'id; ls -la /mnt'
第六步:确认构建和仓库边界
docker --context NAME build -t example:test .
docker --context NAME images example
docker buildx ls
docker buildx inspect --bootstrap
如果使用 Compose:
docker --context NAME compose config
docker --context NAME compose ps
docker --context NAME compose logs
这套顺序将故障分成“选择错误、网络错误、认证错误、Daemon 权限错误、远程路径错误、构建器错误和应用错误”,避免把所有问题都归因于 TLS 或 SSH。
结语
Docker Context 是 Docker CLI 的连接选择和配置抽象;远程 Daemon 是真正持有容器、镜像、网络和卷状态的执行端。SSH 通过远程登录和 socket 转发建立连接,TLS 通过证书保护并认证 Docker Engine API。两者都不能替代 Docker Daemon 的权限治理,也不会自动同步客户端文件、环境变量或凭据。
使用远程 Context 时,最重要的因果链是:
Context 选择 endpoint
→ SSH/TLS 建立 API 通道
→ 远程 Daemon 验证并执行请求
→ 远程主机的内核、文件系统、网络和存储决定结果
只要按照这条链区分客户端、传输层、Daemon 主机、构建器和容器内部环境,就能正确解释远程路径为何不存在、镜像为何不在本地、权限为何与 SSH 登录不同,以及为什么一个看似普通的 Docker socket 权限实际上具有接近宿主机 root 的影响范围。
系列导航与关联阅读
- 系列入口:Docker 完整学习路线:从镜像与容器到安全、可观测和生产交付
- 上一篇:Docker Inspect、Exec 与调试:Namespace、环境、进程和最小镜像
- 下一篇:Rootless Docker:User Namespace、网络、存储、限制和迁移
- 延伸:Docker Engine API 与自动化:SDK、事件、权限和幂等操作
- 延伸:Docker Daemon 配置:data-root、日志、代理、镜像源、TLS 和验证
官方资料
本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。

评论
0 条讨论