镜像标签是与镜像引用关联的可读名字。如果仓库策略没有禁止重新指派,同一个标签之后可以标识另一个产物。v1.4 和 latest 的区别首先是命名惯例,而不是前者天然不可变。要复现部署,需要记录实际选择的产物,以及影响行为的平台和运行配置。只保存一个看起来像版本号的名字,并不足以证明当时运行的是哪些内容。
坏掉的样子 latest 不会要求仓库寻找最新上传镜像或最高语义版本,它是在引用省略标签时采用的惯例默认值。发布另一个版本,不会自动移动 latest;必须有人或自动化更新这个具体引用。如果把每次 push 都解释成自动更新 latest,就会错误理解发布流程,甚至误判为什么一个节点仍然取得旧版本。
标签的本地解析结果,也可能不同于仓库当前结果。拉取策略、本地镜像是否存在,以及编排工具的行为,共同决定何时查询仓库。因此,两台机器运行相同可读标签,仍可能使用不同产物。仅记录命令文本,会留下实际取回什么、是否保留缓存引用等不确定性;排查时应读取所选产物身份。
移动标签不会改写已经创建的容器所选择的镜像。重启该容器,会保留原配置的镜像和可写状态,不会按照标签的新目标重新创建实例。如果更新流程只是重新打标签,然后 restart 旧容器,命令可能成功,应用却仍运行旧内容。标签更新、容器替换和容器重启需要分别判断,不能混成同一个动作。
快速验证 先取得固定基础镜像,再用 Python 和 Docker 构建器运行脚本。实测环境是 Docker 29.3.1、Linux arm64。所有标签使用独特的本地名字,不向仓库推送任何内容;容器关闭网络,清理只删除实验自己的容器与标签。因此可以实际验证移动行为,又不改变已有发布引用:
from pathlib import Path from tempfile import TemporaryDirectory import json,subprocess,uuid BASE='python@sha256:528257d48c1da0dcecc2e725d1ae34498d60c965f1241e39cd6a85a8859bdf84' def docker(*args,check=True): result=subprocess.run(['docker',*args],capture_output=True,text=True,timeout=120) if check:assert result.returncode==0,result.stderr return result def inspect(name,kind='image'):return json.loads(docker(kind,'inspect',name).stdout)[0] name='xbcatm-tags-'+uuid.uuid4().hex[:10];tags=[name+':one',name+':two',name+':release',name+':latest'];container=name+'-old' try: with TemporaryDirectory(prefix=name+'-') as directory: root=Path(directory) (root/'Dockerfile').write_text(f'FROM {BASE}\nCOPY version.txt /version.txt\nCMD ["cat", "/version.txt"]\n') for tag,version in zip(tags,['one','two']): (root/'version.txt').write_text(version+'\n');docker('build','--network','none','-t',tag,str(root)) one=inspect(tags[0])['Id'];two=inspect(tags[1])['Id'];assert one!=two docker('image','tag',tags[0],tags[2]);docker('image','tag',tags[0],name) assert inspect(tags[2])['Id']==inspect(tags[3])['Id']==one docker('create','--name',container,'--network','none',tags[2]) docker('image','tag',tags[1],tags[2]) assert inspect(tags[2])['Id']==two and inspect(tags[3])['Id']==one assert inspect(container,'container')['Image']==one assert docker('start','--attach',container).stdout.strip()=='one' assert docker('run','--rm','--network','none',tags[2]).stdout.strip()=='two' assert docker('run','--rm','--network','none',tags[3]).stdout.strip()=='one' print('moving release changes newly created containers; the existing container still runs its original image') print('omitting a tag selected latest, but updating release did not update latest') docker('image','rm',tags[2]);assert inspect(tags[1])['Id']==two print('removing one tag leaves another reference to the same image intact; all operations stayed local') finally: docker('rm','-f',container,check=False);docker('image','rm',*tags,check=False) 脚本构建两个镜像,/version.txt 分别包含 one 与 two,镜像身份不同。它先让 release 指向第一份镜像,再用没有显式标签的仓库名标记同一镜像。检查确认,省略标签会得到 latest,而这两个引用最初都选择第一份镜像。这里直接验证默认标签规则,不靠名字含义猜测。
...