3.2 拉取容器镜像
当我们执行 docker pull alpine:3.18 拉取容器镜像时,会发生哪些事情?
- 解析命令和提取镜像名称和标签:解析 alpine:3.18,将其解析为镜像名称 alpine 和标签 3.18,未指定标签时默认使用 latest。
- 与 Registry 通信(默认使用 dockerhub),获取镜像清单文件 Manifest:从镜像仓库下载 alpine:3.18 镜像的元数据文件 (Manifest),该文件包含了镜像的层级结构、层级 ID、大小等信息。
- 检查本地缓存,跳过已存在的镜像层:根据 Manifest 文件中的层级 ID,检查本地主机上是否已经存在相同的镜像层。如果存在,则直接使用本地镜像层,否则进入下一步。
- 下载缺失的镜像层:从镜像仓库下载本地主机上不存在的镜像层,镜像层是只读的,每个层级都包含了文件系统的变化。
- 存储下载的镜像层:将下载的镜像层存储到本地的镜像存储库中,每个层都是独立存储的,并且可以被多个镜像共享。
- 更新本地镜像索引:将
alpine:3.18
的镜像标记指向下载的层。 - 确认下载完成:确认镜像已经成功下载,此时可以使用
docker images
命令查看下载的镜像
不过在实际使用过程中,还是会遇到各种问题,这里记录下我常见的几种情况。
拉取容器镜像时,最常见的问题都是网络不通或者拉取速度缓慢,这个时候有几种办法可以解决问题:
- 使用代理:
- 为 dockerd 配置 HTTP/HTTPS 代理:dockerd 在执行网络请求时可以通过代理绕过防火墙,或者使用速度更快的链路访问镜像仓库。
- 在网关配置透明代理:如果 dockerd 所在的宿主机网络环境中存在软路由,可以配置分流规则,让访问镜像仓库的请求都经过代理服务器。
- 使用 mirror:可以将 registry 配置为一个 pull-through 缓存,将访问该 registry 的请求转发到 dockerhub 并缓存响应。
透明代理的使用不做展开,其他两种办法的详细操作如下:
与普通程序一样,dockerd 也支持读取 HTTP_PROXY、HTTPS_PROXY、NO_PROXY 这些环境变量,如果使用 systemd 运行 Docker,可以做以下配置:
创建 docker.service.d 目录
mkdir -p /etc/systemd/system/docker.service.d
创建代理配置文件
假设在本地启动了一个 http 代理,监听 2080 端口,我们希望 Docker 访问外网的流量都经过代理,则配置如下:
cat <<EOF > /etc/systemd/system/docker.service.d/http-proxy.conf
[Service]
Environment="HTTP_PROXY=http://127.0.0.1:2080"
Environment="HTTPS_PROXY=http://127.0.0.1:2080"
Environment="NO_PROXY=localhost,127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
EOF
重启dockerd
systemctl daemon-reload
systemctl restart docker
操作完成后可以在 docker info 的输出中看到配置的代理
➜ ~ docker info
Client: Docker Engine - Community
Version: 26.0.2
...
...
Server:
...
HTTP Proxy: http://127.0.0.1:2080
HTTPS Proxy: http://127.0.0.1:2080
No Proxy: localhost,127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
...
如果不具备自建容器镜像加速器的条件,可以尝试我维护的站点:hub.alduin.net,它提供了对 Docker Hub 的镜像加速,其他常用镜像仓库加速可以参考:常用容器镜像仓库。
这里会使用开源项目 github.com/distribution/distribution 中的 registry 组件作为 mirror 服务。我会在一台境外的 VPS 上搭建 mirror 服务,通过 caddy 反向代理暴露到公网使用。
拉取镜像
docker pull registry:2
启动mirror服务
mkdir mirror
cd mirror && mkdir registry
docker run --name mirror -d -e REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io -v $PWD/registry:/var/lib/registry -p 5000:5000 registry:2
配置caddy暴露mirror服务
这里使用 hub.alduin.net 作为域名,将访问该域名的请求转发到监听本地 5000 端口的 mirror 服务
cat <<EOF > Caddyfile
hub.alduin.net {
encode zstd gzip
tls /etc/cert/alduin.crt /etc/cert/alduin.key
log {
output file /var/log/mirror.log
}
reverse_proxy localhost:5000
}
EOF
然后重启 caddy
配置dockerd
编辑 /etc/docker/daemon.json 添加 registry-mirrors,如下:
➜ ~ cat /etc/docker/daemon.json
{
"registry-mirrors": ["https://hub.alduin.net"]
}
重启 docker
systemctl daemon-reload
systemctl restart docker
验证 docker info 输出
➜ ~ docker info
Client: Docker Engine - Community
Version: 26.0.2
...
...
Server:
...
Insecure Registries:
127.0.0.0/8
Registry Mirrors:
https://hub.alduin.net/
Live Restore Enabled: false
验证mirror是否正常
本地拉取 alpine:3.18 镜像
➜ ~ docker pull alpine:3.18
3.18: Pulling from library/alpine
619be1103602: Already exists
Digest: sha256:11e21d7b981a59554b3f822c49f6e9f57b6068bb74f49c4cd5cc4c663c7e5160
Status: Downloaded newer image for alpine:3.18
docker.io/library/alpine:3.18
检查 mirror 服务日志,可以看到镜像被成功缓存
➜ mirror docker logs -f --tail=100 mirror
172.17.0.1 - - [12/Jun/2024:08:25:33 +0000] "GET /v2/ HTTP/1.1" 200 2 "" "docker/26.0.2 go/go1.21.9 git-commit/7cef0d9 kernel/6.8.4-2-pve os/linux arch/amd64 UpstreamClient(Docker-Client/26.0.2 \\(linux\\))"
time="2024-06-12T08:25:33.974565682Z" level=info msg="response completed" go.version=go1.20.8 http.request.host=hub.alduin.net http.request.id=a44ab6ef-a74e-4323-ad92-296b826e0908 http.request.method=GET http.request.remoteaddr=120.1.2.3 http.request.uri="/v2/" http.request.useragent="docker/26.0.2 go/go1.21.9 git-commit/7cef0d9 kernel/6.8.4-2-pve os/linux arch/amd64 UpstreamClient(Docker-Client/26.0.2 \(linux\))" http.response.contenttype="application/json; charset=utf-8" http.response.duration="783.122µs" http.response.status=200 http.response.written=2
time="2024-06-12T08:25:35.031615937Z" level=info msg="response completed" go.version=go1.20.8 http.request.host=hub.alduin.net http.request.id=05451dde-5a96-4731-aba2-d0ed369ba018 http.request.method=HEAD http.request.remoteaddr=120.1.2.3 http.request.uri="/v2/library/alpine/manifests/3.18" http.request.useragent="docker/26.0.2 go/go1.21.9 git-commit/7cef0d9 kernel/6.8.4-2-pve os/linux arch/amd64 UpstreamClient(Docker-Client/26.0.2 \(linux\))" http.response.contenttype="application/vnd.docker.distribution.manifest.list.v2+json" http.response.duration=502.022825ms http.response.status=200 http.response.written=1638
172.17.0.1 - - [12/Jun/2024:08:25:34 +0000] "HEAD /v2/library/alpine/manifests/3.18 HTTP/1.1" 200 1638 "" "docker/26.0.2 go/go1.21.9 git-commit/7cef0d9 kernel/6.8.4-2-pve os/linux arch/amd64 UpstreamClient(Docker-Client/26.0.2 \\(linux\\))"
time="2024-06-12T08:25:35.599495237Z" level=info msg="response completed" go.version=go1.20.8 http.request.host=hub.alduin.net http.request.id=bfffaaf6-7594-41b7-bc2b-4343409fbede http.request.method=GET http.request.remoteaddr=120.1.2.3 http.request.uri="/v2/library/alpine/manifests/sha256:11e21d7b981a59554b3f822c49f6e9f57b6068bb74f49c4cd5cc4c663c7e5160" http.request.useragent="docker/26.0.2 go/go1.21.9 git-commit/7cef0d9 kernel/6.8.4-2-pve os/linux arch/amd64 UpstreamClient(Docker-Client/26.0.2 \(linux\))" http.response.contenttype="application/vnd.docker.distribution.manifest.list.v2+json" http.response.duration=2.466805ms http.response.status=200 http.response.written=1638
172.17.0.1 - - [12/Jun/2024:08:25:35 +0000] "GET /v2/library/alpine/manifests/sha256:11e21d7b981a59554b3f822c49f6e9f57b6068bb74f49c4cd5cc4c663c7e5160 HTTP/1.1" 200 1638 "" "docker/26.0.2 go/go1.21.9 git-commit/7cef0d9 kernel/6.8.4-2-pve os/linux arch/amd64 UpstreamClient(Docker-Client/26.0.2 \\(linux\\))"
time="2024-06-12T08:25:36.352563815Z" level=info msg="Adding new scheduler entry for library/alpine@sha256:695ae78b4957fef4e53adc51febd07f5401eb36fcd80fff3e5107a2b4aa42ace with ttl=167h59m59.99999688s" go.version=go1.20.8 instance.id=ca7ffc02-e97e-42a8-a86c-8100ded10f9c service=registry version=2.8.3
time="2024-06-12T08:25:36.352701655Z" level=info msg="response completed" go.version=go1.20.8 http.request.host=hub.alduin.net http.request.id=2c09a993-3848-4300-b15b-d7febb386cf6 http.request.method=GET http.request.remoteaddr=120.1.2.3 http.request.uri="/v2/library/alpine/manifests/sha256:695ae78b4957fef4e53adc51febd07f5401eb36fcd80fff3e5107a2b4aa42ace" http.request.useragent="docker/26.0.2 go/go1.21.9 git-commit/7cef0d9 kernel/6.8.4-2-pve os/linux arch/amd64 UpstreamClient(Docker-Client/26.0.2 \(linux\))" http.response.contenttype="application/vnd.docker.distribution.manifest.v2+json" http.response.duration=187.948203ms http.response.status=200 http.response.written=528
172.17.0.1 - - [12/Jun/2024:08:25:36 +0000] "GET /v2/library/alpine/manifests/sha256:695ae78b4957fef4e53adc51febd07f5401eb36fcd80fff3e5107a2b4aa42ace HTTP/1.1" 200 528 "" "docker/26.0.2 go/go1.21.9 git-commit/7cef0d9 kernel/6.8.4-2-pve os/linux arch/amd64 UpstreamClient(Docker-Client/26.0.2 \\(linux\\))"
time="2024-06-12T08:25:37.21476761Z" level=info msg="response completed" go.version=go1.20.8 http.request.host=hub.alduin.net http.request.id=0fea14bb-4e39-4dd7-86d2-8f4b57f61fb2 http.request.method=GET http.request.remoteaddr=120.1.2.3 http.request.uri="/v2/library/alpine/blobs/sha256:d3782b16ccc94322a5c5a7d004192b5daa2a1ecd61c143074e36dba844408e1c" http.request.useragent="docker/26.0.2 go/go1.21.9 git-commit/7cef0d9 kernel/6.8.4-2-pve os/linux arch/amd64 UpstreamClient(Docker-Client/26.0.2 \(linux\))" http.response.contenttype="application/octet-stream" http.response.duration=303.799677ms http.response.status=200 http.response.written=1472
172.17.0.1 - - [12/Jun/2024:08:25:36 +0000] "GET /v2/library/alpine/blobs/sha256:d3782b16ccc94322a5c5a7d004192b5daa2a1ecd61c143074e36dba844408e1c HTTP/1.1" 200 1472 "" "docker/26.0.2 go/go1.21.9 git-commit/7cef0d9 kernel/6.8.4-2-pve os/linux arch/amd64 UpstreamClient(Docker-Client/26.0.2 \\(linux\\))"
time="2024-06-12T08:25:37.297546046Z" level=info msg="Adding new scheduler entry for library/alpine@sha256:d3782b16ccc94322a5c5a7d004192b5daa2a1ecd61c143074e36dba844408e1c with ttl=167h59m59.9999972s" go.version=go1.20.8 instance.id=ca7ffc02-e97e-42a8-a86c-8100ded10f9c service=registry version=2.8.3
如果小规模使用,registry 完全可以满足需求,只需要一行命令就可以在本地搭建起镜像仓库
docker run --name registry -d -p 5000:5000 --restart always registry:2
然后选择一个镜像打 tag 即可推送到本地镜像仓库
➜ ~ docker tag d6b2c32a0f14 localhost:5000/registry:2
➜ ~ docker push localhost:5000/registry:2
The push refers to repository [localhost:5000/registry]
e8d8a60f42b5: Pushed
8135430299ba: Pushed
b2533b493853: Pushed
7908a7c94e63: Pushed
aedc3bda2944: Pushed
2: digest: sha256:a9ea4b79e84f7c4a771c4a5b1a93d4542b1d1e2a1c575ad504e54ba53d220045 size: 136
但如果需要多用户、大规模私有化部署,目前唯一的选择就是 Harbor。Harbor 也同样使用 registry 存储容器镜像,但添加了 core、redis、postgres、chartmuseum 等组件来提供更丰富的功能。
在 Harbor 的版本发布页 github.com/goharbor/harbor/releases 可以下载到离线安装包,默认使用 docker-compose 来部署服务,也可以使用 helm 在 K8s 中部署 Harbor:github.com/goharbor/harbor-helm。
Docker 本身是一个 C/S 架构的服务,docker 命令行中也没有关于禁用 TLS 校验访问不安全的镜像仓库的标志位,只能通过配置 dockerd 来添加不安全的镜像仓库,有两种办法配置。
修改/etc/docker/daemon.json
在配置文件中添加 insecure-registries 来访问 HTTP 和自签名证书的 HTTPS 镜像仓库
➜ ~ cat /etc/docker/daemon.json
{
"insecure-registries": [
"https://registry.example.com",
"http://10.0.0.1:5000"
]
}
配置 /etc/docker/certs.d/
适用于访问自签名证书的 HTTPS 镜像仓库,在 /etc/docker/certs.d/ 下创建与镜像仓库同名的目录,然后在目录下放置自签名时使用的 CA 证书,如果需要双向验证,可以一起放置 client 证书和密钥
/etc/docker/certs.d/ <-- 证书目录
└── localhost:5000 <-- 镜像仓库
├── client.cert <-- 客户端证书(可选)
├── client.key <-- 客户选密钥(可选)
└── ca.crt <-- 根证书