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 本质上保存了:

  1. endpoint 类型和地址;
  2. TLS CA、客户端证书和私钥等连接材料的位置或内容;
  3. 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

发生的事情是:

  1. 本地 CLI 根据 prod-ssh 找到 SSH endpoint;
  2. CLI 通过 SSH 建立到远程主机的连接;
  3. 远程主机上的 Docker Daemon 接收 POST /containers/create 等 API 请求;
  4. 远程 Daemon 在自己的 Linux 内核、文件系统、网络和存储中创建 Alpine 容器;
  5. 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 最容易出现的问题,不是连接失败,而是命令成功执行却作用在错误环境。应区分:

  1. 客户端 shell 环境:执行 docker 的机器;
  2. Docker CLI 配置环境:Context、凭据、插件和环境变量;
  3. Daemon 主机环境:远程文件系统、内核、代理、DNS、镜像存储;
  4. 容器或构建环境:容器内变量、构建参数、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、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。