首页  /  云原生  /  正文

Dockerfile 进阶实战与镜像发布

云原生 2026-10-02📖 16 分钟👁 —
🐳 云原生 · Docker第 8 / 9 页123456789📖 完整导航 →

一、实战:自己做一个 tomcat 镜像

这部分偏开发,要懂点 Java 和 tomcat。运维视角学它主要看命令格式和部署方式,所以我这里只写命令,没装 JDK 演示测试。

准备两样东西:tomcat 压缩包(以及它依赖的 jdk),和一份 Dockerfile。

# Dockerfile 是标准的官方写法,build会自动寻找该文件不需要手动 -f

FROM centos
MAINTAINER zzz<1234@qq.com>

COPY readme.txt /usr/local/readme.txt
# ADD 命令添加的会自动解压,后面跟上解压位置
ADD jdk-8u11-linux-x64.tar.gz /usr/local/
ADD apache-tomcat-9.0.22.tar.gz /usr/local/

RUN yum -y install vim

ENV MYPATH /usr/local
WORKDIR $MYPATH

ENV JAVA_HOME /usr/local/jdk1.8.0_11
ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
ENV CATALINA_HOME /usr/local/apache-tomcat-9.0.22
ENV CATALINA_BASE /usr/local/apache-tomcat-9.0.22
ENV PATH $PATH:$JAVA_HOME/bin:$CATALINA_HOME/lib:$CATALINA_HOME/bin

EXPOSE 8080
CMD /usr/local/apache-tomcat-9.0.22/bin/startup.sh && \
  tail -F /usr/local/apache-tomcat-9.0.22/logs/catalina.out

几个点:

构建和运行:

docker build -t diytomcat .

docker run -d -p 8081:8080 \
  -v /home/zzz/build/tomcat/test:/usr/local/apache-tomcat-9.0.22/webapps/test \
  -v /home/zzz/build/tomcat/logs:/usr/local/apache-tomcat-9.0.22/logs \
  diytomcat

顺便说一句,原笔记这里写的是 /url/local/...(少了 s),而且两行 -v 之间漏了续行符 \、挂载目标也少写了一级,直接复制会拼成一条错命令,上面已经按 /usr/local/apache-tomcat-9.0.22/... 修好了。

接下来去挂载目录看有没有内容、curl 测一下、浏览器访问一下。因为做了挂载,发布项目直接在 Linux 里写就行,不用重新构建镜像:

在挂载进容器的项目目录里建 WEB-INF/web.xml 和 index.jsp,宿主机改完容器里立刻生效

然后看 catalina.out 的日志(略)。这一块不是运维的内容,看个大概的格式排版和测试方式就够了。

再练一个:把微服务 jar 打成镜像

顺手把后面 compose 要用的那个微服务镜像也构建出来。先把打好的 jar 传上去:

/mydocker 目录下的 docker_boot-0.0.1-SNAPSHOT.jar

然后写 Dockerfile。这里的 ADD 相当于把 jar 包拷进镜像并顺便改了个名字:

微服务的 Dockerfile:FROM java:8,VOLUME /tmp,ADD 改名 jar,ENTRYPOINT 启动,EXPOSE 6001

对照着看,ADD 源路径 目标路径 就是这个文件在做的事 —— 一般会把需要的文件都放在同一个目录,所以源路径直接写文件名;里面的 VOLUME /tmp 就是前面说过的"在镜像里声明一个挂载点"。

构建和运行:

docker build -t zzyy_docker:1.6 .
docker run -d -p 6001:6001 zzyy_docker:1.6

如果 docker 被防火墙拦了,先把防火墙关掉、重启 docker 再执行。起来之后本地测一下接口,能看到返回就说明镜像没问题:

docker run 起容器后 curl 127.0.0.1:6001/order/docker 返回 hello docker

这个微服务后面会被写成 compose 项目,一条命令连 Redis、MySQL 一起拉起来 —— Compose 那一页用的就是它。

二、构建优化:缓存、.dockerignore、多阶段构建

前面说过,每条指令都是一层,所以镜像怎么变小、构建怎么变快,全在"层"上做文章。

1. 用好吃构建缓存

Docker 逐层构建,某一层没变就直接复用缓存,一旦某层变了,它后面的所有层全部重建。

所以顺序很讲究:把不常变的东西放前面,经常变的放后面。比如一个 Node 项目:

# 好:依赖没变就不重装
COPY package.json package-lock.json ./
RUN npm ci
COPY . .

# 差:改一行源码,上面的 npm ci 也白装了
COPY . .
RUN npm ci

RUN 该合并的也要合并,用 && 串起来能少一层,顺手把包管理器的缓存清掉:

RUN yum makecache && yum install -y vim net-tools && yum clean all

上一页那个 centos 的 Dockerfile 就是反例:每装一个包起一层,层数太多。同样的需求换成 ubuntu 底包、再把几条 RUN 合成一条,层数和体积都能降下来:

FROM ubuntu
LABEL maintainer="zzz<zzz@qq.com>"
# 新版的作者写法,MAINTAINER 已经过时了

ENV MYPATH /usr/local
WORKDIR $MYPATH

RUN apt-get update && apt-get install -y \
    net-tools \
    iproute2 \
    inetutils-ping \
    && rm -rf /var/lib/apt/lists/*
# 清除缓存

EXPOSE 80

CMD ["/bin/bash"]

原来那三条 RUN apt-get install -y xxx 被 && 串成了一条,rm -rf /var/lib/apt/lists/* 也在同一层里把缓存删掉 —— 不删的话缓存会跟着这一层一起被打进镜像。

2. .dockerignore

构建的时候 Docker 会把整个目录(构建上下文)打包发给引擎,构建输出里那句 => [internal] load .dockerignore 就是干这个的:

 => [internal] load .dockerignore                                                                                         0.0s
 => => transferring context: 2B                                                                                           0.0s

写一个 .dockerignore 把不用进镜像的东西排除掉,.git、node_modules、日志、本地配置都写进去,上下文小了构建会快很多:

.git
node_modules
*.log
.env

3. 那两个构建警告

回到第 7 页那次构建,结尾有三条警告,其实是在提醒两件事:

 - JSONArgsRecommended: JSON arguments recommended for CMD to prevent unintended behavior related to OS signals (line 5)
 - MultipleInstructionsDisallowed: Multiple CMD instructions should not be used in the same stage because only the last one will be used (line 5)
 - JSONArgsRecommended: JSON arguments recommended for CMD to prevent unintended behavior related to OS signals (line 6)

4. 多阶段构建

编译型的项目(Go、Java、前端)最烦的是把整个编译工具链也打进了运行镜像里。多阶段构建就是在一个 Dockerfile 里写多个 FROM,每个 FROM 是一个阶段,用 AS 起名字,最后只把产物拷进运行阶段:

# 构建阶段:带着完整的 Go 工具链,只在这里编译
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /out/app ./cmd/app

# 运行阶段:只拷二进制,工具链一点都进不来
FROM alpine:3.20
COPY --from=builder /out/app /usr/local/bin/app
ENTRYPOINT ["app"]

前端的套路一样:一个阶段 npm run build 出静态文件,另一个阶段 FROM nginx 再把 dist 拷进去。这样最终镜像从几百兆、上 G 掉到几十兆,顺手还少了 COPY . . 把源码带进生产镜像的隐患。

5. 构建失败留下的虚悬镜像

镜像之间是一层层共用的,构建失败、或者构建时忘了写 -t,就会留下仓库名和标签都是 none 的镜像,这种叫虚悬镜像:

docker images 里 REPOSITORY 和 TAG 都是 none 的虚悬镜像

留着没用还占地方,直接清掉重新构建:

docker image prune
# prune 译为修剪

三、发布到 Docker Hub

  1. 先注册一个账号。Hub 需要梯子(没有也能用但是很慢),实在麻烦而且大概率用不到,我这边只是演示。
  2. 在服务器上先提交登录,否则 push 不上去(毕竟没登录)。
root@VM-0-9-ubuntu:/home/dockerfile# docker login --help
Usage:  docker login [OPTIONS] [SERVER]

Authenticate to a registry.
Defaults to Docker Hub if no server is specified.

Options:
  -p, --password string   Password or Personal Access Token (PAT), or "-" to read from stdin
      --password-stdin    Take the Password or Personal Access Token (PAT) from stdin
  -u, --username string   Username

登录命令就长这样,看到 Login Succeeded 就成了:

docker login -u 用户名,输密码后提示 Login Succeeded

然后 push:

docker push 作者/镜像名:版本号

最好带上版本号,不带可能被拒绝。另外 push 的镜像必须是本地已经有的,想改版本号得先 tag 再 push。tag 命令是新增一个版本,不删原来的:

先 docker tag 换成 用户名/镜像名:1.0,再 docker push,推送时一层层 Preparing

发布也是按层级结构上传的,所以只有变化的层才真的传。虚拟机环境下几乎发不上去,必须翻墙。

四、发布到阿里云容器镜像服务

没有阿里云账号,而且用得少,这里只把流程记一下,具体照官方文档走。

  1. 登录阿里云
  2. 找到容器镜像服务
  3. 创建命名空间做隔离,防止命名冲突
在容器镜像服务里创建命名空间,命名空间用来隔离不同项目的镜像
  1. 创建容器镜像
创建镜像仓库:选地域、选命名空间,再填仓库名称和摘要
  1. 浏览页面信息,访问仓库地址
仓库基本信息里红框圈出的三行就是公网、专有网络和经典网络三个仓库地址
  1. 按页面上的操作步骤执行,无非还是 login / tag / push 那三条命令
页面上给出的四步操作:登录、拉取、推送、选择仓库地址
  1. 成功了就能看到镜像版本
推送成功后仓库里出现 v1.0 版本,状态正常

以后发布项目就是发布 Docker 镜像,别人 docker pull 然后 run 就能跑起来自己的项目了。

五、整个流程串一遍

Dockerfile 经 build 变成 Images,Images 再 push 到 registry,或者 save 成 tar 备份
Docker 命令全景图:镜像、容器、宿主机文件、tar 包、Dockerfile、仓库各管一摊

图片是视频里的流程图,配合前面的命令看一遍就顺了。load 和 save 这两个命令自己用 help 学就行,是把镜像导成 tar 包再导回来用的。

到这里为止,Docker 的日常使用就没什么大问题了。想再深入就得看 Docker 网络,那一块才是偏运维的,下一页开始。