Docker 私有仓库这件事,我拖了挺久才动手。一开始总觉得有公共镜像源就够了,直到有次给一个离线环境部署服务,现场没有外网,镜像拉不下来,临时找台机器导出导入,来回折腾了大半天。那次之后我就把私有仓库搭起来了,顺带把容器重启策略也系统梳理了一遍——这两件事看着不搭,实际上经常一起出现:仓库解决“镜像从哪来”,重启策略解决“容器挂了怎么办”,一个管供给,一个管自愈,凑在一起才是一套能落地的部署底座。
这篇内容适合两类人看:一类是刚接触容器化部署、想在自己机器或内网环境里搭一个私有镜像仓库的;另一类是已经会用 Docker 跑服务,但容器一重启就出问题、搞不清no、on-failure、always、unless-stopped到底该选哪个的。我会把仓库搭建的完整链路讲清楚,包括为什么这么配、哪些参数容易踩坑,然后把四种重启策略逐个拆开,配上实际验证过程。全程都是我在真实环境里跑过的操作,命令可以直接抄。
1. 为什么私有仓库不是“可有可无”的组件
1.1 公共镜像源解决不了的三类场景
很多人对私有仓库的第一反应是“我又不发镜像给别人,要它干嘛”。这个想法在单机开发阶段没问题,但只要跨过这个阶段,就会撞上墙。我总结下来,公共源解决不了的主要是三类场景。
第一类是离线与内网环境。生产环境、实验环境、客户现场,很多是不通外网的。这时候你的镜像必须有一个内网可访问的存放点,否则每次部署都得靠docker save导出 tar 包、拷贝、docker load导入,镜像一多就是灾难。我见过一个项目有十几个微服务镜像,每次发版靠 U 盘拷,版本还对不上,排查问题时根本说不清现场跑的是哪个包。
第二类是镜像版本的可控性。公共源上的镜像标签可能被覆盖,latest更是随时在变。你今天拉的和明天拉的很可能不是同一个东西,出问题时无法复现。私有仓库里你可以自己定标签规范,配合不可变标签策略,保证“这个标签对应的镜像永远是这个”。
第三类是构建产物的私密性。业务镜像里往往打包了配置文件、内部依赖,这些东西不适合放到公共源上。私有仓库天然就是访问可控的,谁能推、谁能拉,自己说了算。
提示:私有仓库不等于“必须搞一套复杂的认证体系”。小团队内部用,基础认证加内网隔离就够用了,别一上来就上重型方案,维护成本会把收益吃掉。
1.2 Registry 与 Harbor 的取舍逻辑
搭私有仓库,绕不开一个选型问题:用官方的registry镜像,还是用 Harbor 这类带管理界面的方案。我的建议很直接——先问自己要不要 Web 界面、要不要多用户权限、要不要镜像扫描。
官方registry镜像的特点是小、轻、启动快,一个容器起来就能用,配置文件就那几个字段。缺点是它基本只提供“存和取”的能力,没有图形界面,权限控制也比较基础(靠 htpasswd 做基础认证),镜像清理得自己写脚本调 API。适合个人、小团队、内网简单场景。
Harbor 则是企业级方案,带 Web 控制台、基于角色的权限、镜像复制、漏洞扫描、标签保留策略等。代价是组件多,部署起来要一堆容器,资源占用也高,维护门槛明显上升。
我给个粗略的判断表,方便对照:
| 维度 | 官方 registry | Harbor |
|---|---|---|
| 部署复杂度 | 低,单容器 | 高,多组件编排 |
| 资源占用 | 很小 | 较大 |
| Web 管理界面 | 无 | 有 |
| 多用户与角色权限 | 基础认证 | 完整 RBAC |
| 镜像扫描 | 无 | 内置 |
| 适用场景 | 个人、小团队、内网 | 企业、多团队、合规要求 |
我自己的做法是:先用官方 registry 把链路跑通,把推送拉取、认证、存储位置这些基础问题吃透,再考虑要不要升级到 Harbor。因为很多人在 Harbor 上卡住,根子其实是对仓库的基本机制不理解,换个大方案照样卡。
1.3 仓库在整体部署链路里的位置
把私有仓库放进部署链路看,它的角色很清晰:构建端推、运行端拉。开发机或 CI 构建出镜像,打上仓库地址/项目名/镜像名:标签的完整名字,推送到私有仓库;目标机器从私有仓库拉取运行。
这里有个特别容易忽略的点:镜像的完整名称里,仓库地址是硬编码进去的。也就是说,你docker build时打的标签如果没带仓库地址,后面推送时就得重新tag一次。我建议在构建阶段就直接打完整标签,省掉中间那步,也避免“推上去的名字和本地名字对不上”这种低级混乱。
另外,仓库地址的写法有讲究。用域名或 IP 加端口的形式,比如registry.example.internal:5000,Docker 会默认按 HTTPS 去连;如果仓库没配证书,就得在客户端的daemon.json里把它标记为不安全仓库,否则推送直接报错。这个坑后面会专门讲。
2. 从零搭一个可用的 Registry 服务
2.1 存储目录与配置文件的准备思路
官方 registry 容器默认把镜像存在容器内的/var/lib/registry,容器一删数据就没了。所以第一件事是把存储目录挂到宿主机上。我习惯在宿主机建一个固定目录,比如/data/registry,权限给足,然后通过-v挂进去。
配置文件这块,很多人直接用默认配置跑,能跑通,但一旦要调存储、调认证、调删除功能,就得有配置文件。我建议一开始就写一个config.yml,把关键项显式写出来,别依赖默认值。一个够用的最小配置大概长这样:
version: 0.1 storage: filesystem: rootdirectory: /var/lib/registry delete: enabled: true http: addr: :5000这里有两个点值得说。storage.filesystem.rootdirectory指定容器内存储路径,和挂载点对应上就行。delete.enabled: true是开启删除能力——注意,这只是允许删除,不代表能直接删,删除镜像还得调 API 做垃圾回收,这个后面细说。不开这个开关,你连删的资格都没有。
至于http.addr,默认监听 5000 端口。这个端口在容器内是固定的,映射到宿主机时可以用别的端口,比如-p 5000:5000或-p 8443:5000,看你的端口规划。
2.2 启动容器与端口映射的实际操作
配置写好,启动命令本身不复杂:
docker run -d \ --name registry \ --restart=unless-stopped \ -p 5000:5000 \ -v /data/registry:/var/lib/registry \ -v /data/registry-config/config.yml:/etc/docker/registry/config.yml \ registry:2逐项说一下意图。-d后台运行;--name给个固定名字方便管理;--restart=unless-stopped让仓库容器自己也能自愈(这里正好呼应后面的重启策略,仓库服务本身也得考虑挂了怎么办);-p 5000:5000把容器端口映射到宿主机;两个-v分别挂数据目录和配置文件。
启动后验证一下:
docker ps | grep registry curl http://127.0.0.1:5000/v2/curl返回{}就说明服务起来了,/v2/是 registry 的健康检查端点。如果返回连接拒绝,先看容器是否在跑,再看端口有没有被占用。
注意:这里用 HTTP 访问是没问题的,因为是在本机。但一旦涉及跨机器推送,Docker 客户端默认要求 HTTPS,HTTP 仓库会被拒绝。解决办法是在客户端配置
insecure-registries,这个在下一节展开。
2.3 客户端信任配置:insecure-registries 的正确写法
跨机器推送时最常见的报错就是http: server gave HTTP response to HTTPS client。原因很明确:Docker 客户端默认用 HTTPS 连仓库,而你的仓库只提供 HTTP。
解决方式是在客户端(不是仓库服务器)的 Docker 配置里声明这是不安全仓库。配置文件位置通常是/etc/docker/daemon.json,内容:
{ "insecure-registries": ["registry.example.internal:5000"] }改完重启 Docker 服务:
systemctl daemon-reload systemctl restart docker这里有几个实操细节。第一,insecure-registries里写的地址必须和推送时用的地址完全一致,包括端口。你写192.168.1.10:5000,推送时用localhost:5000,那就不生效。第二,这个配置是客户端行为,每台要推送或拉取的机器都得配,不是配一次全局生效。第三,生产环境更推荐给仓库配 TLS 证书,用正规 HTTPS,insecure-registries只适合内网过渡。
我踩过一次坑:配了insecure-registries但忘了重启 Docker,推送一直报同样的错,查了半天以为是配置写错了,其实是没生效。所以改完配置一定要systemctl restart docker,并用docker info确认配置已经加载。
2.4 推送拉取全流程与标签命名规范
仓库跑起来、客户端信任配好,就可以推拉了。完整流程是这样:
# 给本地镜像打完整标签 docker tag myapp:1.0 registry.example.internal:5000/team/myapp:1.0 # 推送 docker push registry.example.internal:5000/team/myapp:1.0 # 在另一台机器拉取 docker pull registry.example.internal:5000/team/myapp:1.0标签命名我建议遵循仓库地址/项目或团队/镜像名:版本的结构。中间那层“项目或团队”不是必须的,但加上之后镜像多了好归类,也方便后面做权限隔离(如果升级到 Harbor,这层正好对应 project)。
版本标签别只用latest。我的习惯是每次构建打两个标签:一个语义化版本如1.0.3,一个latest指向最新。部署时用具体版本号,保证可复现;latest只用于本地快速验证。这样出问题时能明确知道现场跑的是哪个版本。
推送完成后可以验证仓库里有没有:
curl http://registry.example.internal:5000/v2/_catalog curl http://registry.example.internal:5000/v2/team/myapp/tags/list第一个列出所有仓库名,第二个列出某个镜像的所有标签。这两个接口在排查“推上去了但拉不到”时特别有用。
3. 仓库运维中那些文档不写的细节
3.1 镜像删除为什么删不干净
这是私有仓库最容易被吐槽的点:明明调了删除 API,_catalog里也看不到了,但磁盘空间没释放。原因在于 registry 的存储机制——删除只是删了清单(manifest),底层的层数据(layer)还在,需要额外做垃圾回收(garbage collection)才能真正释放。
完整删除流程分两步。先删 manifest:
# 先拿到 manifest 的 digest curl -I -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \ http://registry.example.internal:5000/v2/team/myapp/manifests/1.0响应头里的Docker-Content-Digest就是 digest,然后:
curl -X DELETE http://registry.example.internal:5000/v2/team/myapp/manifests/<digest>第二步做垃圾回收,这个要在仓库容器内执行,且最好先停掉仓库服务或确保没有推送进行:
docker exec registry /bin/registry garbage-collect /etc/docker/registry/config.yml注意:垃圾回收期间仓库最好只读或停止写入,否则可能出现正在推送的镜像层被误删。我一般安排在低峰期做,做完再恢复服务。
还有个细节:垃圾回收默认不会删除“没有被任何 manifest 引用但仍在仓库里”的 blob 之外的东西,某些情况下需要加-m参数处理 manifest 层面的清理。具体参数看 registry 版本,执行前先--help看一眼,别照抄网上的老命令。
3.2 磁盘空间监控与清理节奏
私有仓库的磁盘增长往往超出预期,因为每次构建推送都是一个新层,旧层不会自动消失。我见过一个仓库几个月涨到几百 G,全是历史版本堆的。
我的做法是定两个规矩。第一,标签保留策略:每个镜像只保留最近 N 个版本,更老的定期清理。这个可以写脚本调 API 实现,也可以人工按季度清一次。第二,磁盘水位告警:给存储目录设个阈值,比如到 80% 就提醒,别等写满了才发现推不上去。
监控命令很简单:
du -sh /data/registry df -h /data前者看仓库占用,后者看分区剩余。如果仓库和数据盘在同一个分区,还要留意其他服务别把空间挤占。
3.3 认证与访问控制的轻量做法
内网仓库不加认证,等于谁都能推镜像,风险不小——万一有人推了个同名镜像覆盖掉,运行端拉到的就是错的。轻量做法是用 htpasswd 做基础认证。
先生成密码文件:
mkdir -p /data/registry-auth docker run --rm --entrypoint htpasswd httpd:2 -Bbn username password > /data/registry-auth/htpasswd然后在 registry 配置里加认证段:
auth: htpasswd: realm: basic-realm path: /etc/docker/registry/auth/htpasswd启动容器时把密码文件挂进去,重启服务。之后推送拉取都要先docker login:
docker login registry.example.internal:5000输入用户名密码后,凭据会存在本地,后续操作自动带上。这里提醒一句:基础认证走的是明文传输(HTTP 下),所以要么在内网可信环境用,要么配 TLS。别在不可信网络上裸跑基础认证。
4. 四大重启策略:不是选“最好”,是选“最合适”
4.1 先搞清楚 restart 策略管的是什么
很多人对重启策略有误解,以为它管的是“容器里的进程挂了怎么办”。准确说,它管的是容器主进程退出后,Docker 守护进程要不要把它重新拉起来。理解这个边界很重要,因为策略生效的前提是容器真的退出了,如果进程还在但卡死不响应,重启策略是感知不到的,那得靠健康检查配合。
四种策略:no、on-failure、always、unless-stopped。默认是no,也就是不自动重启。这个默认值经常被忽略,导致有人以为容器挂了会自动恢复,结果服务停了半天没人知道。
设置方式有两种,启动时指定:
docker run -d --restart=always --name myapp myapp:1.0或者对已存在的容器更新:
docker update --restart=unless-stopped myapp第二种在“容器已经跑起来了但忘了加策略”时特别有用,不用删了重建。
4.2 no 与 on-failure:什么时候“不重启”才是对的
no策略就是不自动重启,容器退出就退出。听起来很“废”,但它有明确适用场景:一次性任务、调试中的容器、以及你希望人工介入判断的场合。
比如跑一个数据迁移脚本,跑完就该退出,你绝不希望它退出后又被拉起来重跑一遍。再比如调试阶段,容器崩了你想看现场日志和退出码,自动重启反而把现场搅乱了。这类场景用no是清醒的选择,不是偷懒。
on-failure则是有条件重启:只有容器以非零退出码退出时才重启。正常退出(退出码 0)不重启。它还能指定最大重试次数:
docker run -d --restart=on-failure:3 --name batch-job batch-job:1.0意思是失败最多重试 3 次,超过就不再拉。这个策略适合“跑任务”类容器——成功了就该结束,失败了重试几次还有救,重试多次还失败说明是代码或环境问题,继续拉也没意义。
我实际用下来的体会是:on-failure最容易被误用在常驻服务上。常驻服务正常运行时不会退出,一旦退出基本就是异常,用on-failure看似合理,但它对“正常退出”不重启,而有些服务被信号正常终止后退出码是 0,这时候就不重启了,跟你期望的“服务挂了要拉起”不一致。所以常驻服务我更倾向always或unless-stopped。
4.3 always 与 unless-stopped:常驻服务的两种自愈姿态
always是最“强硬”的策略:只要容器退出,不管退出码是什么,都重启。包括 Docker 守护进程重启、宿主机重启后,容器也会被拉起来。适合必须常驻的服务,比如 Web 服务、数据库、消息队列。
但它有个“副作用”值得知道:你手动docker stop一个always的容器,它确实会停,但Docker 守护进程重启后它又会被拉起来。也就是说always的容器“停不干净”,除非你docker rm掉或者改策略。这个行为在维护窗口里容易造成困惑——明明停了,重启机器后它又活了。
unless-stopped就是为解决这个困惑设计的:行为跟always基本一致,但有一个例外——如果容器是被手动停止的,那么即使守护进程重启,它也不会被自动拉起。换句话说,它记住了“你是主动停的”这个状态。
这个差别在实际运维里很关键。我维护的一台机器上跑着仓库服务和几个业务容器,全用unless-stopped。做维护时docker stop掉,维护完重启机器,停掉的容器保持停止,不会自己冒出来打乱节奏;而没被手动停的容器照常恢复。这个语义比always更符合人的直觉。
四种策略的对比:
| 策略 | 退出码 0 退出 | 非零退出 | 守护进程重启后 | 手动 stop 后重启机器 |
|---|---|---|---|---|
| no | 不重启 | 不重启 | 不恢复 | 不恢复 |
| on-failure | 不重启 | 重启(可限次数) | 视情况 | 不恢复 |
| always | 重启 | 重启 | 恢复 | 恢复(停不干净) |
| unless-stopped | 重启 | 重启 | 恢复 | 保持停止 |
4.4 策略与健康检查的配合,以及几个实测坑
重启策略有个天然短板:它只看进程退没退出,不看进程“活得好不好”。一个进程还在但已经死锁、不响应请求的容器,重启策略完全无感。这时候要配合健康检查(HEALTHCHECK),让 Docker 定期探测,探测失败可以把容器标记为 unhealthy。但要注意,unhealthy 本身不会触发重启,需要外部机制(比如编排系统或监控脚本)根据健康状态做处理。
实测中我踩过几个坑,列出来供参考。
第一个坑:策略设了但没生效。原因是容器是用docker run起的,但策略参数拼错了,比如写成--restart always(少了等号在某些写法下也能识别,但混用容易出错)。建议统一用--restart=xxx的写法,并用docker inspect确认:
docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' myapp第二个坑:on-failure 的次数限制被忽略。on-failure:3里的 3 是“最多重试次数”,但如果你不写数字,默认是无限重试。有人以为不写就是只试一次,结果容器疯狂重启刷日志。要限制就明确写数字。
第三个坑:重启风暴。容器因为配置错误启动即崩,always策略会不停拉起,日志刷屏、CPU 占用飙升。Docker 内部对重启有退避机制(间隔逐渐拉长),但短时间内的密集重启仍会造成困扰。遇到这种情况,先临时把策略改成no或直接docker stop,把配置问题修好再恢复策略。
第四个坑:仓库容器自身的策略选择。仓库服务是常驻服务,理论上always或unless-stopped都行。我选unless-stopped,理由是维护时手动停掉后不会被意外拉起,避免在垃圾回收或迁移存储时被自动重启打断。这个选择没有绝对对错,关键是和你的运维习惯一致。
5. 把仓库和重启策略串成一套可复用的部署习惯
5.1 一份可复用的启动参数清单
把前面这些揉成一个习惯,我给自己的项目定了一套启动参数模板,新服务上线时照着填:
docker run -d \ --name <服务名> \ --restart=unless-stopped \ --health-cmd="<健康检查命令>" \ --health-interval=30s \ --health-retries=3 \ -p <宿主端口>:<容器端口> \ -v <宿主目录>:<容器目录> \ <仓库地址>/<团队>/<镜像名>:<版本>几个固定原则:常驻服务一律unless-stopped;镜像名带完整仓库地址和明确版本号,不用latest;数据目录必须挂出来;健康检查能加就加。这套模板不复杂,但能挡掉大部分“上线后才发现”的问题。
5.2 上线前的自检动作
每次部署前我会过一遍几个检查点,花不了两分钟,但省下的排查时间很可观。仓库连通性用curl打一下/v2/;镜像能不能拉到,先在目标机器docker pull一次;重启策略用docker inspect确认;数据目录挂载用docker inspect看Mounts字段。这几步做完,基本不会出现“部署完才发现拉不到镜像”或者“容器挂了没自愈”的情况。
这套东西搭一次,后面所有服务都受益。私有仓库让镜像有了稳定的来源,重启策略让服务有了基本的自愈能力,两者合起来,才算把容器化部署的地基打稳。我后来在几个不同环境里重复这套流程,每次都是先跑通最小链路,再按需加认证、加清理、调策略,没有一次需要推倒重来。