2、Docker镜像管理:镜像的获取与查看、镜像的构建(docker build)、Dockerfile基础指令(FROM, RUN, CMD, ENTRYPOINT)、镜像的导出与导入、镜像仓库(Docker Hub与私有仓库)

聊到Docker,镜像就是它的灵魂。没有镜像,容器就是个空壳子。我刚开始接触Docker那会儿,最头疼的就是搞不清镜像和容器的关系。说白了,镜像就是模板,容器就是模板跑起来的实例。今天咱们就把镜像管理这块彻底捋清楚。

2.1 镜像的获取与查看

获取镜像最直接的方式就是拉取。Docker Hub上啥都有,从操作系统到各种中间件,一应俱全。

# 拉取最新版Ubuntu
docker pull ubuntu:latest

# 拉取指定版本
docker pull nginx:1.21-alpine

# 查看本地已有镜像
docker images

# 更详细的查看方式
docker image ls

我个人习惯用 docker images,敲起来快。但如果你要写脚本,建议用 docker image ls,语义更清晰。

小技巧:拉取镜像时尽量指定具体版本标签,别用latest。我在生产环境吃过亏——某次latest自动更新了,结果容器启动失败,排查了半天才发现是镜像版本变了。

查看镜像的详细信息也很重要:

# 查看镜像详情
docker inspect ubuntu:latest

# 只看镜像的历史层
docker history ubuntu:latest

docker history 这个命令我特别喜欢。它能让你看到镜像的每一层是怎么构建出来的。你想想看,如果镜像体积突然变大,用这个命令一查,哪层搞的鬼一目了然。

2.2 镜像的构建(docker build)

自己构建镜像,这才是Docker的核心能力。你写个Dockerfile,然后跑 docker build,一个定制化的镜像就出来了。

# 基本构建命令
docker build -t myapp:v1 .

# 指定Dockerfile路径
docker build -f ./docker/Dockerfile -t myapp:v1 .

# 带构建参数
docker build --build-arg VERSION=1.0 -t myapp:v1 .

这里有个坑,我曾经踩过。那个点号 . 是构建上下文路径,不是随便写的。Docker会把整个上下文目录打包发给守护进程。如果你在根目录执行 docker build .,那酸爽……整个硬盘的文件都会被发过去,构建慢得你想哭。

避坑指南:构建上下文越小越好。我一般会在项目根目录放个 .dockerignore 文件,把node_modules、.git这些没用的东西过滤掉。曾经有个同事没加这个,构建一次花了15分钟,加了之后15秒搞定。

2.3 Dockerfile基础指令

Dockerfile就是镜像的配方。咱们把最核心的几个指令讲透。

FROM:一切的基础

每个Dockerfile第一行必须是FROM。它指定了基础镜像,相当于你盖房子的地基。

# 最简形式
FROM alpine:3.18

# 多阶段构建
FROM golang:1.21 AS builder
# ... 编译阶段 ...

FROM alpine:3.18
COPY --from=builder /app/myapp /myapp

多阶段构建这个特性,我强烈推荐你用。它能让最终镜像体积缩小80%以上。我做过一个Go项目,没用多阶段构建时镜像1.2GB,优化后只有15MB。你想想看,这差距有多大。

RUN:执行命令

RUN是在构建镜像时执行的命令。每一条RUN都会创建一个新的镜像层。

# 不推荐:每条RUN一层,镜像臃肿
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim

# 推荐:合并成一条RUN
RUN apt-get update && \
    apt-get install -y curl vim && \
    rm -rf /var/lib/apt/lists/*

嗯,这里要注意。合并RUN指令不仅能减少层数,还能清理缓存。我见过有人一个Dockerfile写了20条RUN,镜像体积直接爆炸。记住:层数越少,镜像越小。

CMD vs ENTRYPOINT:启动时的区别

这两个指令容易搞混。我刚开始也分不清,后来用了个简单方法记住:

  • CMD:提供默认命令,可以被覆盖
  • ENTRYPOINT:固定入口,不容易被覆盖
# CMD示例:启动nginx
FROM nginx:alpine
CMD ["nginx", "-g", "daemon off;"]

# ENTRYPOINT示例:固定入口
FROM python:3.11
ENTRYPOINT ["python"]
CMD ["app.py"]

# 最佳实践:组合使用
FROM alpine:3.18
ENTRYPOINT ["/bin/myapp"]
CMD ["--config", "/etc/myapp/config.yaml"]

为什么推荐组合使用?我举个例子。你写了个CLI工具,用ENTRYPOINT固定入口,用户可以通过CMD传参。这样既保证了入口不变,又给了用户灵活性。我在做微服务网关时就是这么设计的,效果很好。

核心区别:
  • docker run myapp --help → 如果ENTRYPOINT是python,CMD是app.py,实际执行的是 python --help
  • docker run myapp → 执行 ENTRYPOINT + CMD 的组合
  • docker run --entrypoint bash myapp → 强制覆盖ENTRYPOINT

2.4 镜像的导出与导入

有时候网络不通,或者你想把镜像带到离线环境,就需要导出导入。

# 导出镜像为tar文件
docker save -o myapp.tar myapp:v1

# 压缩导出(推荐)
docker save myapp:v1 | gzip > myapp.tar.gz

# 导入镜像
docker load -i myapp.tar

# 从压缩文件导入
gunzip -c myapp.tar.gz | docker load

我曾经在客户现场部署,那地方连Docker Hub都连不上。提前把镜像导出到U盘带过去,用 docker load 导入,几分钟就搞定了。要是没这功能,现场拉镜像能等到天荒地老。

注意:docker save 和 docker export 不一样。save保存的是完整镜像(含历史层),export只保存容器的文件系统。我建议用save,因为导入后还能保留镜像的标签和历史信息。

2.5 镜像仓库

镜像不能总在本地待着,得有个地方存。Docker Hub是官方的公共仓库,私有仓库则适合企业内部使用。

Docker Hub:公共仓库

# 登录
docker login

# 推送镜像
docker tag myapp:v1 username/myapp:v1
docker push username/myapp:v1

# 拉取别人的镜像
docker pull nginx:alpine

Docker Hub免费用户只能建一个私有仓库。我建议你把敏感镜像放私有仓库,公开的镜像才放Docker Hub。别问我为什么这么说——我见过有人把数据库密码直接写进镜像里还推到公共仓库的,那场面……

私有仓库:Harbor与Registry

企业级应用,私有仓库是标配。Harbor是目前最流行的选择。

# 启动一个简单的Registry
docker run -d -p 5000:5000 --name registry registry:2

# 推送到私有仓库
docker tag myapp:v1 localhost:5000/myapp:v1
docker push localhost:5000/myapp:v1

# 从私有仓库拉取
docker pull localhost:5000/myapp:v1

Harbor的功能更强大,支持镜像复制、漏洞扫描、访问控制。我在公司搭了一套Harbor,配合Jenkins做CI/CD,开发提代码后自动构建镜像并推送到Harbor,测试环境直接拉取部署。整个流程自动化后,发布效率提升了至少3倍。

安全提醒:私有仓库一定要配HTTPS和认证。我见过有人图省事,直接开HTTP不加密码,结果被挖矿程序盯上,服务器CPU跑满100%。血的教训啊。

好了,镜像管理这块就聊到这儿。说白了就是三板斧:拉取构建、导出导入、仓库管理。把这些搞明白了,Docker就算入门了。下一章咱们聊聊容器运行时的那些事儿。