在前两篇文章中,我们掌握了 Docker 的基础概念、环境搭建、镜像和容器的常用操作,以及常用镜像的实战部署。但在实际开发中,官方镜像往往无法满足个性化需求:比如官方 Tomcat 镜像默认无 webapps 文件、需要自定义 JDK+Tomcat 组合、需要将 SpringBoot 项目打包为镜像等。此时就需要定制 Docker 镜像,这是 Docker 高级使用的核心,也是实现应用 “一次打包,到处运行” 的关键。

本文作为系列第三篇,将详细讲解 Docker 定制镜像的两种核心方法:docker commit(提交容器为镜像) 和Dockerfile(编写脚本构建镜像),其中 Dockerfile 是生产环境的首选,需要重点掌握,同时结合实战案例,让你能轻松制作自己的专属镜像。

一、为什么需要定制镜像?

官方 Docker 镜像为了保证通用性和轻量性,通常只包含最基础的运行环境,缺少很多个性化的配置和依赖,在实际开发中存在诸多不便:

  1. 官方 Tomcat 镜像的webapps目录默认为空,需要手动拷贝文件,每次创建容器都要重复操作;
  2. 官方镜像缺少项目所需的依赖包、配置文件,需要进入容器后手动安装;
  3. 需要将自己开发的应用(如 SpringBoot 项目)与运行环境(如 JDK)打包在一起,形成可直接运行的镜像;
  4. 开发、测试、生产环境需要统一的镜像,避免环境差异导致的问题。

定制镜像的核心价值,就是将应用运行所需的环境、依赖、配置、应用程序全部打包在一起,形成一个标准化的镜像,后续只需运行该镜像,即可直接启动应用,无需任何额外配置,真正实现 “一次打包,到处运行”。

二、定制镜像的两种方法

Docker 提供了两种定制镜像的方法,二者各有适用场景,可根据需求选择:

  1. docker commit:基于运行的容器,将容器的修改提交为一个新的镜像,适用于快速调试、临时定制;
  2. Dockerfile:通过编写纯文本的脚本文件,定义镜像的构建步骤,适用于生产环境、标准化构建,是主流方式。

核心原则

无论使用哪种方法,Docker 镜像的构建都遵循分层构建原则:镜像由多个只读层组成,每层对应一个构建步骤,修改仅在最上层的可写层进行,分层构建能有效减少镜像体积,提高构建效率(重复步骤无需重新构建)。

三、方法一:docker commit - 提交容器为新镜像

docker commit的原理非常简单:先基于官方镜像创建容器,在容器中进行个性化修改(如安装依赖、拷贝文件、配置环境),然后将修改后的容器提交为一个新的镜像,后续即可基于该新镜像创建容器,无需重复修改。

该方法的核心类似 Git 的提交,将容器的 “修改记录” 保存为新镜像,优点是简单、快速,缺点是不透明、无法版本控制,生产环境不推荐。

1. 基本语法

docker commit [可选参数] 容器ID/容器名 新镜像名:[标签TAG]
# 核心可选参数
-a="作者信息" # 指定镜像的作者
-m="描述信息" # 指定镜像的修改描述

2. 实战案例:定制带 webapps 的 Tomcat 镜像

官方 Tomcat 镜像的webapps目录默认为空,需要手动将webapps.dist中的文件拷贝到webapps才能访问默认页面,我们通过docker commit将该修改提交为新镜像,后续创建容器直接使用新镜像即可。

步骤 1:基于官方 Tomcat 镜像创建并启动容器

# 拉取官方Tomcat最新版镜像(若未拉取)
docker pull tomcat
# 创建并后台启动Tomcat容器,端口映射8080:8080
docker run -itd --name tomcat-tmp -p 8080:8080 tomcat

步骤 2:进入容器,进行个性化修改

# 进入容器的交互终端
docker exec -it tomcat-tmp /bin/bash
# 进入Tomcat目录,将webapps.dist的文件拷贝到webapps
cd /usr/local/tomcat
cp -r webapps.dist/* webapps
# 验证修改:查看webapps目录是否有文件
ls webapps
# 退出容器(不停止容器)
Ctrl + P + Q

步骤 3:将修改后的容器提交为新镜像

# 查看容器ID(也可直接使用容器名tomcat-tmp)
docker ps
# 提交容器为新镜像,作者:dev,描述:add webapps files,镜像名:mytomcat,标签:1.0
docker commit -a="dev" -m="add webapps files" tomcat-tmp mytomcat:1.0

步骤 4:验证新镜像并创建容器

# 查看本地镜像,确认mytomcat:1.0已存在
docker images
# 基于新镜像创建Tomcat容器,端口映射8081:8080
docker run -itd --name tomcat-custom -p 8081:8080 mytomcat:1.0

此时,外网通过http://宿主机IP:8081可直接访问 Tomcat 的默认页面,无需再手动拷贝文件,实现了个性化定制。

3. docker commit 的优缺点

优点

  • 操作简单、快速,无需编写脚本,适合快速调试和临时定制;
  • 基于现有容器修改,直观易懂,适合 Docker 新手。

缺点

  • 不透明:无法知道新镜像的构建步骤,后续维护和排错困难;
  • 无法版本控制:脚本文件可通过 Git 版本控制,而 commit 的修改无记录;
  • 镜像体积大:容器的修改会生成新的层,容易产生冗余,导致镜像体积过大;
  • 不可复现:不同环境下的 commit 操作,可能产生不同的镜像,无法保证一致性。

因此,docker commit仅适用于快速调试和临时定制,生产环境强烈推荐使用 Dockerfile。

四、方法二:Dockerfile - 编写脚本构建镜像(重点)

Dockerfile 是一个纯文本的脚本文件,由一系列大写的指令和对应的参数组成,每个指令对应镜像构建的一个步骤,Docker 会按照 Dockerfile 的指令顺序,自动构建出定制的镜像。

Dockerfile 的核心优势:透明化、可版本控制、可复现、分层构建,是生产环境中定制 Docker 镜像的标准方式,也是企业级开发中必须掌握的技能。

1. Dockerfile 的核心特性

  • 指令大写:Dockerfile 的指令必须大写(如 FROM、RUN、ADD),参数小写,这是规范;
  • 顺序执行:指令按照从上到下的顺序依次执行,每一个指令都会创建一个新的镜像层;
  • 注释用#:行首加 #表示注释;
  • 构建上下文:执行构建命令时,会指定一个构建上下文目录,Dockerfile 中的相对路径基于该目录;
  • .dockerignore:在构建上下文目录中,可创建.dockerignore 文件,指定忽略的文件 / 目录(类似.gitignore),避免无关文件加入镜像。

2. Dockerfile 核心指令(必记)

Dockerfile 的指令有很多,以下为最常用、最核心的指令,掌握这些即可满足 90% 的定制需求,按构建流程分类,便于记忆。

(1)基础镜像指令:FROM(必须是第一条非注释指令)

指定构建镜像的基础镜像,所有定制的镜像都必须基于一个基础镜像,语法:

FROM 镜像名:[标签TAG]
# 示例:基于CentOS7为基础镜像
FROM centos:7
# 示例:基于JDK8为基础镜像
FROM java:8
# 示例:基于官方Tomcat为基础镜像
FROM tomcat:8.5

注意:如果需要制作一个无基础镜像的镜像(如空白镜像),可使用FROM scratch,适用于制作基础系统镜像。

(2)作者信息指令:MAINTAINER(可选)

指定镜像的作者信息(姓名 + 邮箱),语法:

MAINTAINER 作者名<邮箱地址>
# 示例
MAINTAINER dev<dev@163.com>

替代方案:现代 Docker 推荐使用LABEL maintainer="dev<dev@163.com>",功能相同,更灵活。

(3)镜像构建指令:RUN(核心)

在镜像构建过程中,执行指定的命令(如安装软件、创建目录、执行脚本),每一个 RUN 指令都会创建一个新的镜像层,语法有两种:

# 格式1:shell格式,类似在终端执行命令,最常用
RUN 命令
# 格式2:exec格式,适用于命令中有特殊字符的场景
RUN ["命令", "参数1", "参数2"]

# 示例1:安装vim工具
RUN yum -y install vim
# 示例2:创建目录
RUN mkdir -p /usr/local/app
# 示例3:exec格式,执行echo命令
RUN ["echo", "hello docker"]

优化技巧:多个 RUN 指令可通过&&合并为一个,减少镜像层数量,降低镜像体积,示例:

# 合并前:3个RUN指令,3个镜像层
RUN yum -y update
RUN yum -y install vim
RUN yum clean all
# 合并后:1个RUN指令,1个镜像层(推荐)
RUN yum -y update && yum -y install vim && yum clean all

(4)文件复制 / 添加指令:COPY & ADD

将构建上下文目录中的文件 / 目录复制到镜像的指定目录中,二者功能相似,ADD 比 COPY 多一个自动解压功能。

COPY:仅实现文件 / 目录复制

COPY 宿主机文件/目录 镜像内目标路径
# 示例1:将本地的index.html复制到镜像的/usr/share/nginx/html/
COPY index.html /usr/share/nginx/html/
# 示例2:将本地的app目录复制到镜像的/usr/local/
COPY app /usr/local/

ADD:复制 + 自动解压(压缩包)

ADD 宿主机文件/目录/压缩包 镜像内目标路径
# 示例1:复制并自动解压jdk压缩包到镜像的/usr/local/
ADD jdk-8u161-linux-x64.tar.gz /usr/local/
# 示例2:复制并自动解压tomcat压缩包到镜像的/usr/local/
ADD apache-tomcat-8.5.20.tar.gz /usr/local/

使用原则:仅在需要解压压缩包时使用 ADD,其余场景使用 COPY,更直观。

(5)工作目录指令:WORKDIR

指定镜像的工作目录(类似 Linux 的 cd 命令),后续的 RUN、COPY、ADD、CMD 等指令都基于该工作目录执行,若目录不存在,Docker 会自动创建,语法:

WORKDIR 镜像内目录路径
# 示例:设置工作目录为/usr/local
WORKDIR /usr/local
# 后续指令基于/usr/local执行,如创建app目录,实际路径为/usr/local/app
RUN mkdir app

优势:避免使用cd命令,减少镜像层,且工作目录统一,便于维护。

(6)环境变量指令:ENV

在镜像中设置环境变量,后续的指令可使用该变量,容器启动后,也会继承该环境变量,语法:

# 格式1:单个变量
ENV 变量名 变量值
# 格式2:多个变量
ENV 变量名1=变量值1 变量名2=变量值2

# 示例1:设置JDK的安装目录
ENV JAVA_HOME /usr/local/jdk1.8.0_161
# 示例2:设置PATH环境变量,结合JAVA_HOME
ENV PATH $PATH:$JAVA_HOME/bin
# 示例3:多个变量
ENV MY_PATH /usr/local APP_NAME myapp

优势:通过变量管理路径、版本等信息,便于后续修改,提高 Dockerfile 的可维护性。

(7)端口暴露指令:EXPOSE

声明容器运行时需要暴露的端口,仅作声明,不会自动完成端口映射,语法:

EXPOSE 端口号1 端口号2
# 示例:暴露Tomcat的8080端口
EXPOSE 8080
# 示例:暴露MySQL的3306端口和Nginx的80端口
EXPOSE 3306 80

注意:EXPOSE 仅为声明,告诉使用者该镜像的容器需要暴露哪个端口,实际运行容器时,仍需通过-p参数完成端口映射。

(8)容器启动指令:CMD & ENTRYPOINT(核心,二选一)

指定容器启动时执行的命令,二者功能相似,核心区别:CMD 的命令可被 docker run 的最后一个参数覆盖,ENTRYPOINT 的命令不可被覆盖,只能追加参数。

CMD:可被覆盖的启动命令

语法有三种,最常用shell 格式和exec 格式:

# 格式1:shell格式
CMD 命令
# 格式2:exec格式(推荐)
CMD ["命令", "参数1", "参数2"]
# 格式3:为ENTRYPOINT提供默认参数
CMD ["参数1", "参数2"]

# 示例1:启动Tomcat,shell格式
CMD /usr/local/tomcat/bin/catalina.sh run
# 示例2:启动SpringBoot项目,exec格式
CMD ["java", "-jar", "/app/blog.jar"]

覆盖示例:若 Dockerfile 中指定CMD ["echo", "hello docker"],运行容器时docker run 镜像名 echo hello world,则容器启动时会执行echo hello world,覆盖原有的 CMD 命令。

ENTRYPOINT:不可被覆盖的启动命令

语法只有 exec 格式,推荐使用,确保容器启动时始终执行指定的命令,如需追加参数,可通过docker run --entrypoint或 CMD 配合,语法:

ENTRYPOINT ["命令", "参数1", "参数2"]
# 示例1:启动Nginx,始终执行nginx -g "daemon off;"
ENTRYPOINT ["nginx", "-g", "daemon off;"]
# 示例2:结合CMD提供默认参数
ENTRYPOINT ["java", "-jar"]
CMD ["/app/blog.jar"]

追加参数示例:上述示例 2,运行容器时docker run 镜像名 /app/test.jar,则容器启动时会执行java -jar /app/test.jar,CMD 的默认参数被替换,ENTRYPOINT 的命令保持不变。

使用原则:

  • 若容器启动的命令需要灵活修改,使用 CMD;
  • 若容器启动的命令固定,不希望被覆盖,使用 ENTRYPOINT(生产环境首选)。

3. Dockerfile 构建镜像的基本命令

编写完 Dockerfile 后,通过docker build命令构建镜像,语法:

docker build [可选参数] -t 新镜像名:[标签TAG] 构建上下文目录
# 核心可选参数
-f # 指定Dockerfile的路径(若Dockerfile不在构建上下文根目录,或名称不是Dockerfile)
--build-arg # 设置构建时的临时环境变量
# 构建上下文目录:指定为. 表示当前目录,Docker会读取该目录下的Dockerfile和相关文件

示例:在 Dockerfile 所在目录执行构建命令,构建镜像mytomcat:2.0:

docker build -t mytomcat:2.0 .

构建过程中,Docker 会逐行执行 Dockerfile 的指令,每执行一条指令,会显示Step X/Y,并创建一个新的镜像层,最终显示Successfully built 镜像ID和Successfully tagged 新镜像名:TAG,表示构建成功。

五、Dockerfile 实战案例(重点)

以下为三个经典的 Dockerfile 实战案例,覆盖日常开发的绝大多数场景,包括定制基础 Tomcat 镜像、定制 JDK+Tomcat 组合镜像、定制 SpringBoot 项目镜像,可直接参考修改使用。

案例 1:简单定制 - 带默认页面的 Tomcat 镜像

需求:基于官方 Tomcat8.5 镜像,创建自定义的 index.html 页面,容器启动后可直接访问该页面。

步骤 1:创建构建上下文目录,编写 Dockerfile 和 index.html

# 1. 创建构建目录并进入
mkdir -p /home/docker/tomcat-custom && cd /home/docker/tomcat-custom
# 2. 编写index.html文件
echo '<h1>Hello Docker! This is my custom Tomcat.</h1>' > index.html
# 3. 编写Dockerfile文件,命名为Dockerfile(默认名称,无需-f指定)
vim Dockerfile

步骤 2:编写 Dockerfile 内容

# 基础镜像:官方Tomcat8.5
FROM tomcat:8.5
# 作者信息
MAINTAINER dev<dev@163.com>
# 工作目录:Tomcat的webapps/ROOT目录
WORKDIR /usr/local/tomcat/webapps/ROOT
# 复制本地的index.html到镜像的工作目录
COPY index.html .
# 暴露8080端口
EXPOSE 8080
# 容器启动命令:启动Tomcat
CMD ["/usr/local/tomcat/bin/catalina.sh", "run"]

步骤 3:构建镜像

# 在构建目录下执行,构建镜像mytomcat:8.5
docker build -t mytomcat:8.5 .

步骤 4:运行镜像,验证效果

# 创建容器,端口映射8080:8080
docker run -itd --name mytomcat85 -p 8080:8080 mytomcat:8.5

外网通过http://宿主机IP:8080可直接看到自定义的页面Hello Docker! This is my custom Tomcat.,构建成功。

案例 2:进阶定制 - JDK8+Tomcat8.5 组合镜像

需求:基于 CentOS7 基础镜像,手动安装 JDK8 和 Tomcat8.5,设置环境变量,定制一个完整的 JDK+Tomcat 组合镜像,适用于需要统一基础环境的场景。

步骤 1:准备文件,创建构建目录

# 1. 创建构建目录并进入
mkdir -p /home/docker/jdk-tomcat && cd /home/docker/jdk-tomcat
# 2. 上传JDK8和Tomcat8.5的压缩包到该目录(自行下载)
# jdk-8u161-linux-x64.tar.gz、apache-tomcat-8.5.20.tar.gz
# 3. 编写Dockerfile文件
vim Dockerfile

步骤 2:编写 Dockerfile 内容

# 基础镜像:CentOS7
FROM centos:7
# 作者信息
MAINTAINER dev<dev@163.com>
# 安装vim工具(方便后续调试)
RUN yum -y install vim && yum clean all
# 添加并解压JDK8压缩包到/usr/local
ADD jdk-8u161-linux-x64.tar.gz /usr/local/
# 添加并解压Tomcat8.5压缩包到/usr/local
ADD apache-tomcat-8.5.20.tar.gz /usr/local/
# 设置环境变量:MYPATH
ENV MYPATH /usr/local
# 工作目录:MYPATH
WORKDIR $MYPATH
# 设置JDK环境变量
ENV JAVA_HOME $MYPATH/jdk1.8.0_161
ENV CLASS_PATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
# 设置Tomcat环境变量
ENV CATALINA_HOME $MYPATH/apache-tomcat-8.5.20
ENV CATALINA_BASE $MYPATH/apache-tomcat-8.5.20
# 设置PATH环境变量
ENV PATH $PATH:$JAVA_HOME/bin:$CATALINA_HOME/bin:$CATALINA_HOME/lib
# 暴露Tomcat8080端口
EXPOSE 8080
# 容器启动命令:启动Tomcat
ENTRYPOINT ["$CATALINA_HOME/bin/catalina.sh", "run"]

步骤 3:构建镜像

# 构建镜像jdk-tomcat:8.5
docker build -t jdk-tomcat

更多推荐