Docker 这套东西,入门看着简单,几条命令就能跑起来,可真要把"本地 build 一个镜像,推到 Docker Hub,再管好它的历史和清理"这条链路走顺,踩坑的人一抓一大把。镜像构建出来动辄一两个 G、push 到一半卡死、docker history 密密麻麻看不懂、删镜像删了半天下不去空间——这些我都亲身经历过。这篇就把本地连接 Docker Hub、镜像构建、构建历史查看、镜像推送、镜像删除这五个动作从头到尾捋一遍,每一步都讲清楚为什么这么做,而不是丢几条命令让你自己猜。不管你是刚接触容器的后端同学,还是想把镜像发布流程标准化下来的运维,照着做都能直接落地。我会先讲环境自检和凭证配置,再拆 Dockerfile 的设计取舍,接着进入构建实操和构建历史解读,然后是推送的规范化,最后是删除清理策略,中途会穿插大量实测参数和文档里不写的注意事项。
1. 本地环境与 Docker Hub 连接准备
动手构建之前,先把地基打牢。很多人一上来就docker build,结果报错连原因都看不懂,本质是环境没摸清楚。这一节我们不急着造镜像,先把本地 Docker 状态、账号凭证、镜像拉取速度这三件事搞定,后面每一步都会顺畅很多。
1.1 本地 Docker 环境自检与版本确认
第一件事,确认你的 Docker 是"客户端 + 守护进程"都在正常跑。执行docker version,重点看两块输出:Client 段和 Server 段。如果 Server 段报Cannot connect to the Docker daemon,说明守护进程没起来,Linux 上一般是systemctl status docker看一眼,Windows/macOS 上就是 Docker Desktop 没启动。很多人把 Client 当成整体,其实真正干活的是 Server(守护进程),构建、拉取、推送全靠它。
docker version docker infodocker info信息量更大,我习惯关注这几个字段:Storage Driver(存储驱动,常见 overlay2)、Cgroup Driver(cgroupfs 或 systemd)、Docker Root Dir(镜像和容器实际存放位置,默认/var/lib/docker)、Registry Mirrors(镜像加速地址)。为什么要看这些?因为镜像构建的分层缓存、删除镜像释放的空间,全都落在这个 Root Dir 下。你在 A 机器上构建很快,换到 B 机器慢得像蜗牛,八成是存储驱动或者磁盘 IO 的差异。
提示:如果
Docker Root Dir所在分区快满了,构建会直接失败,报no space left on device。养成定期df -h /var/lib/docker的习惯,比事后救火强得多。
还有一个容易被忽略的点:确认你的 Docker 版本。低版本(比如 19.x 以下)对 BuildKit 支持不完整,而 BuildKit 直接决定了构建速度和缓存命中率。跑docker buildx version看看,如果提示没有这个命令,说明你的版本偏老,建议升级到 20.10 以上,构建体验会明显不一样。
1.2 Docker Hub 账号配置与登录凭证
连接 Docker Hub 的第一步是登录,但登录这件事有个坑:明文密码登录在高版本会被拒绝,官方现在主推 Access Token。所以正确姿势是先去 Docker Hub 网页端,在账号设置的 Security 里生成一个 Personal Access Token,权限按需勾选(一般 Read & Write 就够推送用了),然后用它当密码登录。
docker login -u your_username # 回车后粘贴 Access Token 作为密码登录成功后,凭证存哪儿了?Linux 默认在~/.docker/config.json,里面是一段 base64 的用户名和 token,严格说不是加密,只是编码。所以生产环境或者多人共用的机器上,我强烈建议配个凭证助手(credential store),比如把credsStore设成系统密钥链,别让明文躺在文件里。
{ "auths": { "https://index.docker.io/v1/": {} }, "credsStore": "desktop" }排查登录问题的时候,docker logout再docker login是万能第一招;如果推送时报denied: requested access to the resource is denied,基本就是两件事:要么没登录,要么你推送的镜像名不属于你的账号(命名规范问题,后面第 4 节会细说)。这两个原因覆盖了九成以上的权限报错。
1.3 镜像加速与拉取速度优化思路
本地连 Docker Hub 最直观的痛点就是拉镜像慢。这里要区分两件事:拉取(pull)和推送(push)。加速器(Registry Mirror)通常只对拉取生效,推送还是走你本机到官方仓库的网络,所以别指望配了镜像源就推送飞快。
配置加速器的入口是守护进程配置文件/etc/docker/daemon.json(Linux)或 Docker Desktop 的设置界面,写好后重启守护进程生效:
{ "registry-mirrors": ["https://your-mirror.example.com"], "max-concurrent-downloads": 5 }这里max-concurrent-downloads是并发拉取层数,默认 3,网络好可以适当调大,但别贪,调太大反而因为连接竞争变慢。配好之后docker info里的Registry Mirrors会显示出来,验证是否生效。
注意:加速器只解决基础镜像拉取的问题,你的业务镜像还是要老老实实推送到官方仓库。另外,企业内网通常有私有 registry(比如 Harbor),那种情况下你可以直接把镜像推到内网仓库,再在内网之间分发,比走公网快得多,这也是我实际项目里更常用的做法。
环境这一层搞定后,你手里就有了三样东西:一个正常运行的守护进程、一份有效的登录凭证、一个相对顺滑的拉取通道。接下来才轮到真正的核心——镜像怎么造出来。
2. 镜像构建方案设计与 Dockerfile 拆解
构建镜像这件事,新手和老手的差距不在命令敲得多快,而在 Dockerfile 怎么设计。同一个应用,Dockerfile 写得糙,镜像 1.5G、构建 5 分钟、缓存天天失效;写得讲究,镜像 200M、构建几十秒、层复用率高得离谱。这一节我们就掰开揉碎讲清楚方案选型和文件设计。
2.1 为什么要优先考虑多阶段构建
先说结论:只要你的应用需要编译(Java、Go、前端打包、C++ 等),就优先用多阶段构建(multi-stage build)。原因很直接——编译过程需要的工具链(JDK、gcc、node_modules、Maven 缓存)体积极其庞大,但运行时根本用不上。
假设你写一个 Go 服务,单阶段构建大概是:基于 golang 镜像 → 拷贝源码 →go build→ 得到一个几百兆的镜像,里面塞满了编译器和源码。多阶段这么做:
# 第一阶段:构建 FROM golang:1.21 AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -o server . # 第二阶段:运行 FROM alpine:3.19 WORKDIR /app COPY --from=builder /app/server . EXPOSE 8080 CMD ["./server"]最终镜像只包含那个编译好的二进制和一个极小的 alpine 基础镜像,体积能从几百兆压到几十兆。这可不是玄学,是实打实的层裁剪:第一阶段的所有层在最终镜像里根本不出现,COPY --from=builder只搬走你需要的那一个文件。
生活里打个比方:这就像做菜和上菜。厨房(构建阶段)里锅碗瓢盆、调料、案板一应俱全,但端到客人桌上(运行阶段)的只有那盘菜。你不会把整个厨房搬到餐桌上。
2.2 Dockerfile 关键指令与分层缓存机制
镜像的每一层都是只读的,构建时每条会产生文件系统变更的指令(RUN、COPY、ADD)都会生成新层。Docker 构建时从上往下逐条比对缓存,一旦某条指令的内容或它的父层变了,后面的缓存全部失效。这就是"缓存命中率"的核心逻辑,理解了它,你就能解释为什么构建时快时慢。
基于这个逻辑,Dockerfile 有一条铁律:变化频率低的放前面,变化频率高的放后面。
看个反例:
COPY . . # 源码一变,后面全失效 RUN pip install -r requirements.txt每改一行代码,依赖就得重装一遍。改成正例:
COPY requirements.txt . # 依赖清单很少变 RUN pip install -r requirements.txt COPY . . # 源码经常变,放最后依赖清单不动的时候,pip install那层直接命中缓存,构建时间从几分钟变成几秒。这个改动我在项目里做一次,团队构建时长直接砍掉一大半,是性价比最高的优化之一。
再说几个高频指令的取舍:
| 指令 | 作用 | 实操建议 |
|---|---|---|
| RUN | 执行命令并生成新层 | 用&&串联多个命令,合并层 |
| COPY | 复制文件进镜像 | 优先用 COPY,别用 ADD |
| ADD | 复制并支持解压/远程拉取 | 除非要自动解压 tar,否则避免 |
| ENV | 设置环境变量 | 运行时常量用它,构建时变量用 ARG |
| CMD / ENTRYPOINT | 容器启动命令 | ENTRYPOINT 定程序,CMD 给默认参数 |
提示:
RUN apt-get update && apt-get install -y xxx一定要写在一行里。分开写的话,update 那层被缓存后,install 可能装到过期的索引,出现"看着装成功了但版本不对"的诡异问题。
2.3 构建上下文与 .dockerignore 的取舍
docker build命令最后那个点.,代表构建上下文(build context),意思是把这个目录打包发给守护进程。很多人不知道这一步,结果在项目根目录下有个几个 G 的node_modules或.git,每次构建光"上传上下文"就要半天,还以为是网络问题。
解决办法就是.dockerignore,规则跟.gitignore类似:
.git node_modules dist *.log .env Dockerfile*把不需要进镜像的东西排除掉,上下文体积能小一个数量级,构建启动瞬间就轻快了。我见过最夸张的一次,某同事的上下文里有 8G 的测试数据,加个.dockerignore后构建启动从 40 秒降到 1 秒。
这里还有个小细节:Dockerfile本身要不要排除?如果你的 Dockerfile 里不COPY它,那排除掉没影响;但如果某条指令要读取它,就得留着。我一般顺手排除,避免误打包。另外.env这类文件务必排除,否则你的密钥、数据库密码可能直接被打进镜像层里,docker history一翻就露馅了——这个安全隐患我在第 3 节讲构建历史时会再强调。
3. 构建实操与构建历史解读
到这一步,Dockerfile 设计好了,终于可以动手构建。但"能构建出镜像"和"构建得又快又干净"是两回事。这一节我把docker build的核心参数、docker history的读法、以及镜像瘦身的具体手法讲透。
3.1 docker build 常用参数与实操现场
最基础的命令谁都