Docker API入门与实践

2026-03-12 22:12:00
丁国栋
原创 19
摘要:本文记录如何配置和使用Docker API远程访问Docker引擎(包括Docker Swarm)。

本文目录是通过PHP编程语言来控制Docker Swarm集群内容器的生命周期:创建、启动、停止、删除容器。

注:不知道是不是“知识诅咒“的原因,Docker 的官方文档做的那么复杂,那么难用,让一个经验丰富的运维、开发人员也无法很快的理清文档的结构以及要找的信息。在我看来,往往一个公司、团队能做好一个产品,但维护好产品的文档和做好搜索工作却不是一件简单的事情。本来文档的目的就是让人使用产品过程中遇到问题可以自行解决,文档做不好就得强迫用户花费耐心和时间来解决他们的问题。既然费尽心思和精力做了文档,为啥不把文档做得方便使用、搜索更精准和快速呢?AI在这方面是挺有优势的,这也是为什么越来越多的人愿意与AI对话来代替搜索,就是人不愿意等待、搜索和处理,想吃现成的。人就是懒惰、就是贪婪,这是人的本性、是人之常情。想要让人逆而行之只会让人们趋利避害,逼迫他们选择更受欢迎的解决方案。

问:2375这个端口是必须开的么?Docker官方文档是怎么说的?给出权威参考

2375端口不是必须开的,Docker官方强烈建议不要在生产环境中直接暴露2375端口。

1. 官方态度:默认关闭,严禁裸奔

Docker默认只监听Unix Socket(/var/run/docker.sock),默认情况下2375端口是关闭的。如果你在配置中看到 -H tcp://0.0.0.0:2375,这属于非默认的、主动开启的配置

官方核心警告:直接暴露2375端口(即不启用TLS加密)是极其危险的。因为Docker API本身没有内置的身份验证机制,一旦暴露,任何能访问该端口的人都可以像root用户一样控制你的Docker守护进程,这相当于把服务器的root权限拱手让人。因此要启用远程管理,必须配置CA证书、服务端证书和客户端证书,实现双向认证。

参考:

2. 安全替代方案(官方推荐)

如果你确实需要远程管理Docker,官方推荐使用2376端口配合TLS双向认证

端口 协议 安全性 官方建议
2375 HTTP (未加密) 极低 严禁在生产环境使用。仅限开发环境或受信任的内网隔离环境。
2376 HTTPS (TLS加密) 生产环境唯一推荐。必须配置CA证书、服务端证书和客户端证书,实现双向认证。

3. 给你的PHP代码安全建议

结合你之前的PHP编程需求,建议采用以下安全策略:

  • 首选方案(最安全):PHP程序与Docker运行在同一台机器上,直接通过Unix Socket文件(/var/run/docker.sock)通信,完全不需要开端口。
  • 次选方案(远程管理):如果PHP必须远程连接,请务必配置TLS(使用2376端口),并在PHP代码中加载客户端证书进行认证。

注:实际上,php应用程序最期望对接的是RESTful API,而不是 Socket 文件,所以配置 TLS 才是可行的方案。

问: port 2377 是啥?我看Docker swarm开启后就有

2377 端口是 Docker Swarm 集群的“管理端口”,专门用于集群节点之间的通信和集群状态管理。它与你之前提到的 2375 端口(Docker 守护进程 API)在用途和安全性上完全不同。

1. 核心作用:集群的“神经中枢”

当你在主机上执行 docker swarm init 时,Docker 引擎会自动在 2377 端口 启动一个 Swarm 管理器。这个端口负责处理所有与集群管理相关的核心事务:

  • 节点加入:当你执行 docker swarm join 命令时,工作节点(Worker)正是通过这个端口连接到管理节点(Manager)来注册自己的。
  • 集群状态同步:管理节点之间通过此端口进行 Raft 共识算法的通信,以维护集群状态的一致性(如 Leader 选举、服务编排信息同步)。
  • API 端点:它也是 Swarm 模式下的 API 端点,用于接收来自 docker service 等命令的请求。

2. 2377 vs. 2375:关键区别

为了让你更清晰地理解,这里对比一下这两个端口:

端口 2377 (Swarm 管理端口) 2375 (Docker API 端口)
用途 集群管理:节点发现、Raft 共识、服务编排 容器管理:创建/启动/停止容器、拉取镜像
通信对象 集群内的 Manager 节点Worker 节点 外部客户端(如你的 PHP 程序、Docker CLI)
默认状态 开启 Swarm 模式后 自动监听 默认 关闭,需手动配置 daemon.json 开启
安全建议 建议仅对 集群内网 开放,防止外部攻击 严禁 在生产环境直接暴露(无认证)

3. 官方文档与权威参考

Docker 官方文档明确指出,2377 端口是 Swarm 模式正常运行所必需的。在集群搭建时,必须确保所有节点(尤其是 Manager 节点)之间的 2377 端口是互通的,否则节点无法加入集群或无法进行状态同步。

总结:2375 是你用来“控制” Docker 的端口,而 2377 是 Docker Swarm 集群用来“自我管理”的端口。在 PHP 编程中,你通常只需要关注 2375 端口(或更安全的 2376 端口)来发送 API 指令,而 2377 端口由 Docker 集群内部自动维护。

问: 具体如何配置TLS加密的Docker Remote API?请提供详细的步骤和PHP代码示例

配置 Docker Remote API 的 TLS 加密涉及服务端配置(Docker 守护进程)和客户端配置(PHP 代码)两部分。以下是详细的步骤和 PHP 代码示例。

一、服务端配置:生成证书并启用 TLS

在 Docker 服务器上执行以下步骤,生成 CA 根证书、服务器证书和客户端证书。

1. 生成证书文件

建议在 /etc/docker/tls 目录下操作。

# 创建证书目录
mkdir -p /etc/docker/tls && cd /etc/docker/tls
# 1. 生成 CA 根证书(用于签发其他证书)
openssl genrsa -out ca-key.pem 4096
openssl req -new -x509 -days 365 -key ca-key.pem -out ca.pem -subj "/CN=my-ca"
# 2. 生成服务器证书(用于 Docker 守护进程)
openssl genrsa -out server-key.pem 4096
openssl req -new -key server-key.pem -out server.csr -subj "/CN=$(hostname)"
# 使用 CA 签名服务器证书
openssl x509 -req -days 365 -in server.csr -CA ca.pem -CAkey ca-key.pem -out server-cert.pem
# 3. 生成客户端证书(用于 PHP 程序认证)
openssl genrsa -out client-key.pem 4096
openssl req -new -key client-key.pem -out client.csr -subj "/CN=client"
# 使用 CA 签名客户端证书
openssl x509 -req -days 365 -in client.csr -CA ca.pem -CAkey ca-key.pem -out client-cert.pem
# 清理临时文件
rm -f *.csr

CA证书和CA签发的证书在有效年限方面遵循“根短枝长”的原则,即根证书(CA)的有效期通常比它签发的证书(叶证书)更短。这是为了降低根证书泄露带来的长期风险。

以下是具体的年限规定和逻辑:

1. 根证书(CA Certificate)

  • 年限趋势越来越短

  • 具体规定

    • 传统年限:过去通常为 10年20年
    • 现代标准:根据 CA/Browser Forum(行业标准组织)的最新要求,根证书的有效期不应超过 398天(约13个月)。例如,Let's Encrypt 的根证书有效期就是 398 天。
  • 原因:缩短根证书寿命是为了提高安全性。如果根证书私钥泄露,较短的寿命意味着攻击者能利用它的时间窗口更小。

2. 中间证书(Intermediate Certificate)

  • 年限:通常为 5年10年

  • 作用:作为根证书和叶证书之间的缓冲层。根证书离线保存,中间证书负责日常签发,这样即使中间证书泄露,也只需吊销中间证书,无需动用到根证书。

3. 叶证书(Leaf Certificate / End-entity Certificate)

  • 年限:通常为 1年2年

  • 具体规定

    • 公开信任的证书(如网站HTTPS):根据行业标准,有效期已缩短至 398天(约13个月)。
    • 私有/内部证书(如你的Docker TLS证书):通常设置为 1年2年,但具体年限由签发机构(CA)的策略决定。
  • 原因:频繁轮换可以降低私钥泄露的风险,并强制定期进行安全审计。

4. 总结与建议

对于你正在配置的 Docker TLS 证书,建议遵循以下原则:

  • 根证书(ca.pem):建议设置为 1年(365天)。虽然私有环境没有强制要求,但遵循“短命”原则更安全。
  • 服务器/客户端证书:建议设置为 1年(365天)或更短。这样每年都需要重新签发,可以确保只有当前有效的设备才能连接。

自签发的证书没有这种限制,理论上可以设置为 100 年甚至 1000 年。但强烈不建议这样做,因为这会带来巨大的安全风险。

1. 为什么可以设置超长年限?

  • 技术层面:OpenSSL 等工具在生成证书时,-days 参数接受任意正整数。只要系统时间戳不溢出,设置 100 年(36500 天)在语法上是完全可行的。

  • 无监管:自签证书(Self-signed Certificate)仅用于内部系统(如你的 Docker 集群),不依赖公共信任链(如浏览器信任的 CA)。因此,它不受行业标准(如 CA/Browser Forum)的强制约束。

2. 为什么强烈不建议设置 100 年?

虽然技术上可行,但设置超长年限会带来以下致命问题:

  • 私钥泄露风险:证书的核心是私钥。如果私钥泄露,攻击者可以伪装成你的服务器或客户端。如果证书有效期为 100 年,意味着这个泄露的私钥在 100 年内都能被利用,你无法通过证书过期来自然淘汰它。
  • 无法强制轮换:证书过期是强制系统管理员进行安全审计和密钥轮换的“硬性机制”。如果设置为 100 年,管理员可能会忘记更换证书,导致系统长期使用可能已经泄露或不安全的密钥。
  • 吊销机制失效:自签证书通常没有完善的吊销列表(CRL)机制。一旦私钥泄露,除了手动在所有客户端删除信任,几乎没有其他补救措施。而短有效期(如 1 年)可以让你在证书过期后自然“重置”安全状态。

3. 最佳实践建议

对于你的 Docker TLS 证书,建议遵循以下原则:

  • 根证书(CA):设置为 1-2 年。虽然它是自签的,但它是信任链的源头,一旦泄露后果严重。
  • 服务器/客户端证书:设置为 1 年 或更短。这样每年都需要重新签发,可以确保只有当前有效的设备才能连接。

2. 配置 Docker 守护进程

编辑 /etc/docker/daemon.json 文件,配置 Docker 监听 2376 端口并启用 TLS 验证。

{
  "tlsverify": true,
  "tlscacert": "/etc/docker/tls/ca.pem",
  "tlscert": "/etc/docker/tls/server-cert.pem",
  "tlskey": "/etc/docker/tls/server-key.pem",
  "hosts": ["tcp://0.0.0.0:2376", "unix:///var/run/docker.sock"]
}

重要提示:如果系统使用 systemd 管理 Docker,直接在 daemon.json 中配置 hosts 可能不生效。此时需要编辑 systemd 服务文件(如 /lib/systemd/system/docker.service),在 ExecStart 行末尾添加 -H tcp://0.0.0.0:2376

# 重启 Docker 服务
systemctl daemon-reload
systemctl restart docker

注:使用 /etc/docker/daemon.json 文件和使用 systemd 配置都可以,但不要同时使用,会导致服务无法启动。例如选择了在配置文件定义启动参数,就不要再使用 systemd 配置,反之亦然。除非应用程序有优先级设计或者冲突检测等机制。


二、PHP 客户端代码示例

PHP 连接 TLS 加密的 Docker API 主要有两种方式:原生 cURLDocker PHP SDK

方式 1:使用原生 cURL(推荐用于简单操作)

这种方式直接使用 PHP 的 cURL 扩展,通过 HTTPS 发送请求,并指定证书路径。

<?php
$docker_host = 'https://your-docker-server-ip:2376';
$cert_path = '/path/to/your/certs/'; // 存放客户端证书的目录
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => $docker_host . '/version', // 测试连接
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_SSL_VERIFYPEER => true, // 验证服务端证书
    CURLOPT_SSL_VERIFYHOST => 2,    // 严格验证主机名
    CURLOPT_CAINFO => $cert_path . 'ca.pem',        // CA 根证书
    CURLOPT_SSLCERT => $cert_path . 'client-cert.pem', // 客户端证书
    CURLOPT_SSLKEY => $cert_path . 'client-key.pem',   // 客户端私钥
]);
$response = curl_exec($ch);
if (curl_errno($ch)) {
    die('Error: ' . curl_error($ch));
}
curl_close($ch);
echo "Docker Version Info:\n";
echo $response;
?>

方式 2:使用 Docker PHP SDK(推荐用于复杂管理)

使用 Composer 安装 docker-php/docker-php 库,它提供了面向对象的接口。

安装依赖:

composer require docker-php/docker-php

PHP 代码示例:

<?php
require_once 'vendor/autoload.php';
use Docker\Docker;
use Docker\DockerClientFactory;
// 配置连接参数
$config = [
    'remote_socket' => 'tcp://your-docker-server-ip:2376',
    'ssl' => true,
    'ssl_cert' => '/path/to/client-cert.pem',
    'ssl_key' => '/path/to/client-key.pem',
    'ssl_ca' => '/path/to/ca.pem',
    'stream_context_options' => [
        'ssl' => [
            'verify_peer' => true,
            'verify_peer_name' => true,
            'allow_self_signed' => false,
        ]
    ]
];
try {
    $client = DockerClientFactory::create($config);
    $docker = Docker::create($client);
    // 示例:列出所有容器
    $containers = $docker->containerList();
    foreach ($containers as $container) {
        echo $container->getNames()[0] . "\n";
    }
} catch (Exception $e) {
    die('Connection failed: ' . $e->getMessage());
}
?>

三、关键注意事项

  1. 端口区别:TLS 加密使用 2376 端口,而非不安全的 2375 端口。
  2. 证书权限:确保生成的证书文件(特别是私钥 .pem)权限设置为 600(仅所有者可读写),防止泄露。
  3. 防火墙:确保服务器的 2376 端口在防火墙中已放行。
  4. PHP 环境:确保 PHP 已安装并启用 opensslcurl 扩展。

问:curl: (56) OpenSSL SSL_read: OpenSSL/3.0.13: error:0A00045C:SSL routines::tlsv13 alert certificate required, errno 0

这个错误表明服务器(Docker 守护进程)要求客户端提供证书,但客户端(你的 cURL/PHP)没有正确发送证书证书不被信任

一、问题诊断与解决步骤

步骤 1:验证证书路径和权限

# 检查证书文件是否存在
ls -la /path/to/your/certs/
# 应该看到:ca.pem, client-cert.pem, client-key.pem
# 检查权限(私钥必须600)
chmod 600 client-key.pem
chmod 644 client-cert.pem ca.pem
# 验证证书有效性
openssl x509 -in client-cert.pem -text | grep "Subject:"
# 应显示:Subject: CN = client

步骤 2:测试 cURL 命令行

在服务器上先用 cURL 命令行测试:

# 基本测试(应失败)
curl https://localhost:2376/version
# 带证书测试
curl --cert client-cert.pem \
     --key client-key.pem \
     --cacert ca.pem \
     https://localhost:2376/version

如果命令行成功但 PHP 失败,继续看步骤 3。

步骤 3:PHP 代码调试

<?php
$docker_host = 'https://localhost:2376';
$cert_path = '/etc/docker/tls/';
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => $docker_host . '/version',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_VERBOSE => true,  // 启用详细输出
    CURLOPT_STDERR => fopen('/tmp/curl_debug.log', 'w+'),  // 输出到文件
    CURLOPT_SSL_VERIFYPEER => true,
    CURLOPT_SSL_VERIFYHOST => 2,
    CURLOPT_CAINFO => $cert_path . 'ca.pem',
    CURLOPT_SSLCERT => $cert_path . 'client-cert.pem',
    CURLOPT_SSLKEY => $cert_path . 'client-key.pem',
    CURLOPT_SSLKEYPASSWD => null,  // 如果私钥有密码,填在这里
    CURLOPT_SSLCERTTYPE => 'PEM',  // 明确指定类型
    CURLOPT_SSLKEYTYPE => 'PEM',
]);
$response = curl_exec($ch);
if (curl_errno($ch)) {
    echo "cURL Error: " . curl_error($ch) . "\n";
    echo "Error Code: " . curl_errno($ch) . "\n";
    // 输出调试信息
    echo "Debug info saved to /tmp/curl_debug.log\n";
    echo file_get_contents('/tmp/curl_debug.log');
} else {
    echo "Success:\n";
    print_r(json_decode($response, true));
}
curl_close($ch);
?>

二、完整的 PHP TLS 连接类

<?php
class DockerTLSClient {
    private $host;
    private $certPath;
    private $ch;
    public function __construct($host = 'https://localhost:2376', $certPath = '/etc/docker/tls/') {
        $this->host = rtrim($host, '/');
        $this->certPath = rtrim($certPath, '/') . '/';
        $this->validateCertificates();
    }
    private function validateCertificates() {
        $requiredFiles = ['ca.pem', 'client-cert.pem', 'client-key.pem'];
        foreach ($requiredFiles as $file) {
            $fullPath = $this->certPath . $file;
            if (!file_exists($fullPath)) {
                throw new Exception("Certificate file not found: $fullPath");
            }
            if (!is_readable($fullPath)) {
                throw new Exception("Certificate file not readable: $fullPath");
            }
        }
        // 检查私钥权限
        if (substr(sprintf('%o', fileperms($this->certPath . 'client-key.pem')), -4) !== '0600') {
            chmod($this->certPath . 'client-key.pem', 0600);
        }
    }
    public function request($method, $endpoint, $data = null) {
        $this->ch = curl_init();
        $options = [
            CURLOPT_URL => $this->host . $endpoint,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CUSTOMREQUEST => strtoupper($method),
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_CAINFO => $this->certPath . 'ca.pem',
            CURLOPT_SSLCERT => $this->certPath . 'client-cert.pem',
            CURLOPT_SSLKEY => $this->certPath . 'client-key.pem',
            CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_TIMEOUT => 30,
        ];
        if ($data !== null) {
            $options[CURLOPT_POSTFIELDS] = is_string($data) ? $data : json_encode($data);
        }
        curl_setopt_array($this->ch, $options);
        $response = curl_exec($this->ch);
        $httpCode = curl_getinfo($this->ch, CURLINFO_HTTP_CODE);
        if (curl_errno($this->ch)) {
            $error = curl_error($this->ch);
            $errno = curl_errno($this->ch);
            curl_close($this->ch);
            throw new Exception("cURL Error ($errno): $error");
        }
        curl_close($this->ch);
        if ($httpCode >= 400) {
            throw new Exception("HTTP $httpCode: " . $response);
        }
        return json_decode($response, true);
    }
    // 测试连接
    public function testConnection() {
        try {
            $result = $this->request('GET', '/version');
            echo "Connection successful!\n";
            echo "Docker API Version: " . ($result['ApiVersion'] ?? 'N/A') . "\n";
            echo "Docker Version: " . ($result['Version'] ?? 'N/A') . "\n";
            return true;
        } catch (Exception $e) {
            echo "Connection failed: " . $e->getMessage() . "\n";
            return false;
        }
    }
    // 容器操作示例
    public function listContainers($all = false) {
        $endpoint = '/containers/json' . ($all ? '?all=1' : '');
        return $this->request('GET', $endpoint);
    }
    public function createContainer($config) {
        return $this->request('POST', '/containers/create', $config);
    }
    public function startContainer($containerId) {
        return $this->request('POST', "/containers/$containerId/start");
    }
}
// 使用示例
try {
    $docker = new DockerTLSClient('https://your-server-ip:2376', '/path/to/certs/');
    if ($docker->testConnection()) {
        // 列出容器
        $containers = $docker->listContainers();
        print_r($containers);
        // 创建容器示例
        $containerConfig = [
            'Image' => 'nginx:alpine',
            'HostConfig' => [
                'PortBindings' => [
                    '80/tcp' => [['HostPort' => '8080']]
                ]
            ]
        ];
        // $result = $docker->createContainer($containerConfig);
        // $docker->startContainer($result['Id']);
    }
} catch (Exception $e) {
    echo "Error: " . $e->getMessage() . "\n";
}
?>

三、常见问题排查清单

1. 证书生成问题

# 重新生成客户端证书
cd /etc/docker/tls
# 确保使用正确的 CN
openssl req -new -key client-key.pem -out client.csr \
  -subj "/CN=client" \
  -addext "keyUsage = digitalSignature, keyEncipherment" \
  -addext "extendedKeyUsage = clientAuth"
openssl x509 -req -days 365 \
  -in client.csr \
  -CA ca.pem \
  -CAkey ca-key.pem \
  -out client-cert.pem \
  -extfile <(printf "keyUsage=digitalSignature,keyEncipherment\nextendedKeyUsage=clientAuth")

2. Docker 服务端配置检查

# 检查 Docker 是否监听 2376
sudo netstat -tlnp | grep 2376
# 查看 Docker 日志
sudo journalctl -u docker --since "5 minutes ago"
# 验证 Docker 配置
sudo docker --tlsverify \
  --tlscacert=ca.pem \
  --tlscert=server-cert.pem \
  --tlskey=server-key.pem \
  -H=localhost:2376 version

3. PHP 环境检查

<?php
// 检查 PHP 扩展
echo "OpenSSL: " . (extension_loaded('openssl') ? 'Enabled' : 'Disabled') . "\n";
echo "cURL: " . (extension_loaded('curl') ? 'Enabled' : 'Disabled') . "\n";
// 检查 OpenSSL 版本
echo "OpenSSL Version: " . OPENSSL_VERSION_TEXT . "\n";
// 测试证书读取
$cert = file_get_contents('/path/to/certs/client-cert.pem');
echo "Certificate size: " . strlen($cert) . " bytes\n";
?>

四、快速解决方案

如果以上都太复杂,最直接的方法:

# 1. 重新生成所有证书
cd /etc/docker/tls
./generate-certs.sh  # 如果有脚本
# 或使用上面的命令重新生成
# 2. 重启 Docker
sudo systemctl restart docker
# 3. 测试
curl --cert client-cert.pem --key client-key.pem --cacert ca.pem \
  https://localhost:2376/version

如果仍然失败,检查 Docker 配置中的 tlsverify 是否设置为 true,这是强制要求客户端证书的关键设置。

发表评论
博客分类