☰
Docker私有仓库搭建与容器重启策略实战指南
2026/10/28 12:31:47 网站建设 项目流程

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 控制台、基于角色的权限、镜像复制、漏洞扫描、标签保留策略等。代价是组件多,部署起来要一堆容器,资源占用也高,维护门槛明显上升。

我给个粗略的判断表,方便对照:

维度官方 registryHarbor
部署复杂度低,单容器高,多组件编排
资源占用很小较大
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字段。这几步做完,基本不会出现“部署完才发现拉不到镜像”或者“容器挂了没自愈”的情况。

这套东西搭一次,后面所有服务都受益。私有仓库让镜像有了稳定的来源,重启策略让服务有了基本的自愈能力,两者合起来,才算把容器化部署的地基打稳。我后来在几个不同环境里重复这套流程,每次都是先跑通最小链路,再按需加认证、加清理、调策略,没有一次需要推倒重来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询