前言
前面我们在我们的docker容器中安装了 MySQL,Tomcat 和 Nginx 等镜像
这些镜像都是哪里来的,别人能写,我们肯定也能写。所以我们如何自定义镜像
如果我们要研究自己如何做一个镜像,而且把我们写的项目打包上云部署,docker 就是最方便的。把微服务打包成镜像,任何装了 docker 的地方,都可以下载使用,极其方便。
对于开发者和企业而言:
- 微服务部署:将应用打包成镜像后,任何安装 Docker 的环境都能一键运行,解决 “环境不一致” 问题。
- 流程标准化:从开发到部署的流程可固化为:
开发应用 → 编写 Dockerfile → 构建镜像 → 上传仓库 → 下载运行
,极大简化跨环境移植。 - 可移植性:镜像包含应用及所有依赖(如库、配置),摆脱对底层系统的依赖,实现 “一次构建,到处运行”。
所以,我们想要制作镜像,这就涉及到我们如何编写 Dockerfile
什么是Dockerfile
Dockerfile的介绍
dockerfile 是一种用于定义和构建 docker 镜像的文本文件。它包含一个个的指令和参数,用于描述镜像的构建过程,包括基础映像、软件包安装、文件拷贝、环境变量设置等,它用命令来说明要执行什么操作来执行构建镜像,每一个指令都会形成一层 Layer
所以说本质上 Dockerfile 是一个文本文件,通过一系列指令定义镜像的构建过程,包括:
- 基础镜像选择(如基于 CentOS 还是 Ubuntu)
- 软件包安装(如安装 JDK、Python)
- 文件拷贝(如将应用代码复制到镜像中)
- 环境变量配置、端口暴露、启动命令等
它是镜像构建的 “源代码”,通过 docker build
命令可生成可运行的镜像。
通过编写 dockerfile,可以将应用程序、环境和依赖项打包成一个独立的容器镜像,使其可以在不同的环境和平台上运行,实现应用程序的可移植性和可扩展性。
Dockerfile的核心结构如下,一个完整的 Dockerfile 由三部分组成,环环相扣形成镜像构建逻辑:
组成部分 | 作用 | 核心指令示例 |
---|---|---|
基础镜像 | 指定构建的起点,所有操作基于此镜像展开(如
FROM centos:7 ) |
FROM |
构建过程指令 | 定义镜像构建中的操作(安装依赖、拷贝文件、配置环境等) | RUN 、COPY 、ADD 、WORKDIR |
容器启动指令 | 定义容器启动时执行的命令(如启动应用服务器) | CMD 、ENTRYPOINT |
Dockerfile 构建镜像的流程
当我们使用 Dockerfile 构建镜像,大概的构建完整步骤如下

而 Docker 构建镜像时,按 “分层构建” 原则执行每条指令,过程如下:
- 从
FROM
指定的基础镜像启动一个临时容器。 - 执行 Dockerfile 中的第一条指令(如
RUN yum install
),修改容器内容。 - 类似
docker commit
操作,将修改提交为一个新的镜像层。 - 基于新生成的镜像层启动下一个临时的新容器,执行下一条指令。
- 重复步骤 2-4,执行 dockerfile 中的下一条命令,直到所有指令执行完毕,最终生成完整镜像。
关键特性:每个指令对应一个镜像层,层与层之间只读(除最上层容器运行时可写),这种设计让镜像复用(如多个镜像共享基础层)和增量构建(修改某指令仅重新构建该层及后续层)更高效。
从软件的角度来看,dockerfile,docker 镜像与 docker 容器分别代表软件的三个不同阶段。
- dockerfile 是软件的原材料(代码)
- docker 镜像则是软件的交付品(.apk)
- docker 容器则是软件的运行状态(客户下载安装执行)
dockerfile 面向开发,docker 镜像成为交付标准,docker 容器则涉及部署与运维,三者缺一不可

- dockerfile:需要定义一个 dockerfile,dockerfile 定义了进程需要的一切东西。dockerfile 涉及的内容包括执行代码或者是文件、环境变量、依赖包、运行时环境等等。
- docker镜像:在 dockerfile 定义了一个文件之后,docker build 时会产生一个 docker 镜像,当运行 docker 镜像时,会真正开始提供服务;
- docker容器:容器是直接提供服务的。
Dockerfile指令
先看关键字
1 | FROM # 基础镜像,当前新镜像是基于哪个镜像的 |

接下来单独讲一些重要的 dockerfile 命令
指令使用的核心原则
- 分层最小化:合并
RUN
命令,减少镜像层数(每层都会增加镜像体积)。 - 可读性优先:合理使用
WORKDIR
简化路径,用#
添加注释说明指令用途。 - 安全性考虑:通过
USER
切换为非 root 用户,避免容器内过高权限。 - 可维护性:用
ARG
提取可变参数(如版本号),便于后续升级。
基础镜像与构建环境设置
1. FROM
作用:指定基础镜像,是 Dockerfile 的第一条指令,所有后续操作都基于此镜像。
语法:
FROM <镜像名>:<标签>
或FROM <镜像ID>
示例
1
2FROM ubuntu:22.04 # 基于Ubuntu 22.04构建
FROM openjdk:17-jdk-slim # 基于OpenJDK 17的精简版镜像注意
- 若本地无指定镜像,Docker 会自动从仓库拉取。
- 特殊镜像
scratch
表示空镜像(用于构建基础工具,如 busybox)。
2. WORKDIR
作用:设置后续指令的工作目录(类似
cd
命令,影响RUN
、COPY
、CMD
等指令的路径)。语法:
WORKDIR <路径>
示例
1
2WORKDIR /app # 后续指令默认在/app目录下执行
COPY demo.jar ./ # 等价于复制到/app/demo.jar注意
- 目录不存在时会自动创建。
- 建议使用绝对路径(如
/app
),避免相对路径导致的层级混乱。
3. ENV
作用:设置环境变量,可在镜像构建和容器运行时使用。
语法
- 单变量:
ENV <键> <值>
- 多变量:
ENV <键1>=<值1> <键2>=<值2>
- 单变量:
示例
1
2ENV JAVA_HOME /usr/lib/jvm/java-17-openjdk
ENV PATH $JAVA_HOME/bin:$PATH # 追加环境变量注意
- 容器运行时可通过
docker run -e 键=新值
覆盖环境变量。 - 变量可在后续指令中引用(如
COPY $APP_NAME ./
)。
- 容器运行时可通过
文件操作与依赖管理
4. COPY
作用:从宿主机复制文件 / 目录到镜像中。
语法:
COPY <宿主机路径> <镜像内路径>
示例
1
2COPY target/app.jar /app/ # 复制宿主机target目录下的app.jar到镜像的/app目录
COPY ./config /app/config # 复制宿主机当前目录的config目录到镜像的/app/config注意
- 宿主机路径是相对 Dockerfile 所在目录的相对路径(不能用
../
访问父目录)。 - 若镜像内路径不存在,会自动创建父目录。
- 复制目录时,仅复制目录内的内容(不含目录本身),如需保留目录结构需显式指定。
- 宿主机路径是相对 Dockerfile 所在目录的相对路径(不能用
5. ADD
作用:功能类似
COPY
,但支持额外特性:- 自动解压压缩包(如
.tar
、.zip
)到镜像内路径。 - 支持通过 URL 下载文件到镜像中(不推荐,建议用
RUN wget
替代,便于清理缓存)。
- 自动解压压缩包(如
示例
1
2ADD app.tar.gz /app/ # 解压app.tar.gz到镜像的/app目录
ADD https://example.com/demo.sh /tmp/ # 下载文件到/tmp目录注意
- 优先使用
COPY
(功能明确,避免意外解压),仅在需要解压时用ADD
。
- 优先使用
6. RUN
作用:在镜像构建时执行命令(如安装依赖、编译代码),每条
RUN
会创建一个新的镜像层。语法
- shell 格式:
RUN <命令>
(默认在/bin/sh -c
中执行) - exec
格式:
RUN ["可执行文件", "参数1", "参数2"]
(推荐,避免 shell 解析问题)
- shell 格式:
示例
1
2
3
4
5
6
7# 安装依赖并清理缓存(合并命令减少层数)
RUN apt-get update && \
apt-get install -y gcc make && \
rm -rf /var/lib/apt/lists/* # 清理APT缓存,减小镜像体积
# 编译代码(exec格式)
RUN ["gcc", "demo.c", "-o", "demo"]关键
- 用
&&
合并多条命令,减少镜像层数(每层都会占用空间)。 - 及时清理缓存文件(如
apt
、yum
缓存,npm
临时文件)。
- 用
容器运行配置
7. EXPOSE
作用:声明容器运行时计划暴露的端口(仅为文档说明,不实际映射端口)。
语法:
EXPOSE <端口1> <端口2>/<协议>
(默认协议为 TCP)示例
1
2EXPOSE 8080 # 声明暴露8080端口(TCP)
EXPOSE 5000/udp # 声明暴露5000端口(UDP)注意
- 容器启动时需通过
docker run -p 宿主机端口:容器端口
实际映射端口。 - 用于告知使用者该镜像需要暴露哪些端口,增强可读性。
- 容器启动时需通过
8. CMD
作用:定义容器启动时默认执行的命令(可被
docker run
后的命令覆盖)。语法:exec 格式(推荐):
CMD ["可执行文件", "参数1", "参数2"]
- shell 格式:
CMD 命令 参数1 参数2
- shell 格式:
示例
1
2CMD ["java", "-jar", "app.jar"] # 启动Java应用
CMD ["nginx", "-g", "daemon off;"] # 启动Nginx(前台运行)注意
- 一个 Dockerfile 中只能有一条
CMD
,多条时仅最后一条生效。 - 若
docker run
后指定了命令(如docker run 镜像名 bash
),会覆盖CMD
。
- 一个 Dockerfile 中只能有一条
CMD 与 ENTRYPOINT 区别
两个命令都是指定一个容器启动时要运行的命令,但二者有很大的区别
CMD:容器内有多个 CMD 指令时,只有最后一个 CMD 指令会生效,而如果在执行docker run命令时携带了其它命令,将会覆盖掉所有 dockerfile 的 CMD 指令
ENTRYPOINT:ENTRYPOINT 的命令不容易被覆盖。在docker run命令中提供的任何参数都会作为 ENTRYPOINT 命令的参数传递。
当两者组合使用时:那么 CMD 将作为 ENTRYPOINT 的默认参数,如果在
docker run
命令中提供了参数,它将覆盖 CMD 并作为 ENTRYPOINT 的参数传递。
9. ENTRYPOINT
作用:定义容器启动时的固定命令(不可被
docker run
后的命令覆盖,仅能追加参数)。语法
- exec
格式(推荐):
ENTRYPOINT ["可执行文件", "参数1"]
- shell 格式:
ENTRYPOINT 命令 参数1
- exec
格式(推荐):
示例
1
2ENTRYPOINT ["java", "-jar"] # 固定执行java -jar
CMD ["app.jar"] # 默认参数,可被 docker run 镜像名 demo.jar 覆盖场景:
- 用于需要固定启动逻辑的镜像(如工具类镜像,必须执行特定程序)。
- 结合
CMD
可实现 “固定命令 + 可变参数” 的灵活配置
10. VOLUME
作用:声明匿名数据卷(容器运行时自动创建,用于持久化数据,避免容器消亡时数据丢失)。
语法:
VOLUME ["<路径1>", "<路径2>"]
或VOLUME <路径>
示例
1
VOLUME ["/data/mysql"] # 声明/data/mysql为数据卷,存储MySQL数据
注意
- 容器启动时,若挂载路径已有数据,会被复制到数据卷中。
- 可通过
docker run -v 宿主机路径:容器路径
覆盖默认数据卷挂载。
一些高级构建的特性命令内容
11. ARG
作用:定义构建时变量(仅在
docker build
过程中有效,容器运行时不可用)。语法:
ARG <变量名>[=<默认值>]
示例
1
2ARG VERSION=1.0 # 定义默认版本号
RUN wget https://example.com/app-${VERSION}.tar.gz # 构建时引用变量使用:构建时通过
--build-arg
传递参数:1
docker build --build-arg VERSION=2.0 -t myapp:2.0 .
12. ONBUILD
作用:定义 “触发指令”,当当前镜像被其他镜像作为基础镜像时,自动执行该指令。
语法:
ONBUILD <其他指令>
示例
1
2# 在基础镜像中定义
ONBUILD COPY ./app /app # 当其他镜像FROM此镜像时,自动执行COPY场景:用于构建 “基础模板镜像”(如开发环境模板),简化子镜像的构建流程。
13. USER
作用:指定后续指令(
RUN
、CMD
、ENTRYPOINT
等)的执行用户(默认用 root)。语法:
USER <用户名/UID>
示例
1
2
3RUN useradd -m appuser # 创建普通用户
USER appuser # 后续命令以appuser身份执行
CMD ["app"] # 应用程序以非root用户启动,提高安全性注意
- 切换用户前需确保该用户已存在(可通过
RUN useradd
创建)。 - 生产环境中尽量避免用 root 运行应用
- 切换用户前需确保该用户已存在(可通过
如何编写 Dockerfile
以自定义 arch 为例子
由于 Arch Linux 在社区上的版本都是内核级别的,拿过来什么东西都没有,我们就以这个为例子,往Arch中装上我们常用的工具,然后打包发布,以此来学习如何编写 Dockerfile
了解 Arch Linux:Arch 是一个轻量级、滚动更新的 Linux 发行版,适合自定义环境、
目标:构建一个包含vim
、git
、wget
、zsh
等工具的基础
Arch 镜像
首先我们需要编写dockerfile,理论上可以放在任意目录,但从规范、便捷性角度,建议遵循以下逻辑
独立目录存放(推荐)
为 Dockerfile 创建一个专门的项目目录,把构建镜像所需的所有文件(如应用代码、配置文件、脚本等)和 Dockerfile 放在一起。比如要构建自定义 Arch 镜像,可这样组织:
1
2
3
4my-arch-image/
├── Dockerfile # 核心构建文件
├── scripts/ # 可选,存放构建时需要的脚本(如初始化脚本)
└── config/ # 可选,存放配置文件(如 pacman 源配置)构建上下文(
docker build
时传递的目录)明确,避免无关文件被打包进镜像。如果是为某个应用(比如 Python 脚本、Java 程序)构建镜像,直接把 Dockerfile 放在应用代码的根目录。例如一个 Flask 项目:
1
2
3
4my-flask-app/
├── app.py # 应用代码
├── requirements.txt # 依赖清单
└── Dockerfile # 构建镜像的配置- 构建镜像时,能直接通过
COPY . /app
把整个应用代码复制到镜像里,无需额外处理路径。 - 开发、构建流程自然衔接,团队成员 clone 代码后,在目录里直接执行
docker build
就能构建镜像。
- 构建镜像时,能直接通过
我们选择第一个模式,创建好你的目录之后,使用以下命令进入
1 | cd ~/workspace/docker/dockerfileAbout/MyArchDemo |
创建dockerfile并且打开 vim
编辑器,按 i
进入插入模式,输入 :wq
并回车
1 | vim Dockerfile |
然后,其中 Dockerfile 的内容如下
1 | # 1. 指定基础镜像 |
我个人习惯直接链官方的,如果网不太行,可以用这个
1 | # 替换为国内源(清华大学镜像) |
极少数情况下,更换源后可能出现 “密钥验证失败”,可在更换源后添加密钥初始化命令:
1 | RUN echo "Server = https://mirrors.tuna.tsinghua.edu.cn/archlinux/\$repo/os/\$arch" > /etc/pacman.d/mirrorlist \ |
如果需要配置代理。在 Dockerfile 中添加代理设置(替换为实际代理地址):
1 | ENV http_proxy=http://your-proxy:port \ |
每行命令我们仔细剖析一下,来了解编写 Dockerfile 的逻辑
FROM archlinux:latest
- 基础镜像声明:基于官方最新的 Arch Linux 镜像构建
- 官方镜像非常精简,只包含最基础的系统组件
- 如果本地没有这个镜像,
docker build
时会自动从 Docker Hub 下载
LABEL
指令- 用于给镜像添加元数据(如作者、描述)
- 可以通过
docker inspect 镜像名
查看这些信息 - 属于可选指令,但建议添加以提高镜像可维护性
USER root
- 指定后续命令的执行用户(Arch 镜像默认就是 root)
- 安装软件需要 root 权限,所以这里显式指定更清晰
RUN
指令(核心步骤)pacman -Syu --noconfirm
:Arch 的包管理器命令Syu
:S
同步数据库,y
更新系统,u
升级所有包--noconfirm
:自动确认所有操作(非交互式安装)
pacman -S --noconfirm
工具列表:安装指定软件- 列举的都是常用工具:
vim
(编辑器)、git
(版本控制)、wget/curl
(下载工具)等
- 列举的都是常用工具:
pacman -Scc --noconfirm
:清理缓存- 安装软件后会留下缓存文件,清理后能显著减小镜像体积
- 创建普通用户
useradd -m -u 1000 archuser
:创建用户-m
:自动创建用户主目录(/home/archuser
)-u 1000
:指定用户 ID(1000 是 Linux 系统普通用户的常用 ID)
- 安装 sudo 并配置权限:允许普通用户执行管理员命令
- 这是安全最佳实践:容器内尽量避免直接使用 root 运行程序
WORKDIR /home/archuser
- 设置后续命令的工作目录(类似
cd
命令) - 容器启动后会默认进入这个目录
- 设置后续命令的工作目录(类似
USER archuser
- 切换到普通用户(后续命令和容器启动都会用这个用户)
- 再次强调:非 root 用户运行更安全
CMD ["zsh"]
- 容器启动时默认执行的命令:启动 zsh 终端
- 当你运行容器时,会直接进入 zsh 交互界面
Dockerfile 文件已经编写完毕,我们在 Dockerfile 所在目录执行以下命令来构建镜像:
1 | # 构建镜像,标签为custom-arch:v1 |
-t custom-arch:v1
:给镜像命名(custom-arch
)和打标签(v1
).
:表示 Dockerfile 所在的当前目录
构建过程说明:
- Docker 会逐行执行 Dockerfile 中的指令
- 首次构建会下载基础镜像,可能需要几分钟(取决于网络)
- 每一步执行完成后会创建一个镜像层
- 看到
Successfully built <镜像ID>
表示构建成功

欸我去,什么情况,没绷住,我 Docker
没开,没用root,sudo -i
起手忘了,希望大家引以为戒
接下来让他搁这慢慢自己构建着玩去吧

构建完成后,运行容器测试:
1 | # 启动容器并进入交互模式 |
-it
:-i
保持标准输入打开,-t
分配伪终端(交互模式必备)--rm
:容器退出后自动删除(测试用,避免残留无用容器)
第一次启动这其中,会出现如下窗口

这是 Zsh(Z
Shell)的首次配置向导,出现的原因是你的容器里的
archuser
用户是第一次启动
Zsh,且家目录(~
)下没有 Zsh 的启动配置文件(如
.zshrc
、.zprofile
等 )。Zsh 作为更强大的
Shell(比默认的 Bash
功能更丰富),首次运行时会检查用户家目录下的启动脚本(.zshrc
等 )。如果这些文件不存在,就会触发 zsh-newuser-install
向导,帮你初始化配置
如果你想配,就选择1,不想配使用最基础的就选择 0
进入容器后可以验证:
- 查看当前用户:
whoami
→ 应该显示archuser
- 检查工具是否安装:
vim --version
、git --version
- 尝试使用
sudo
:sudo pacman -Ss nginx
(需要输入密码,默认无密码直接回车) - 退出容器:
exit

到这里是构建成功了,可以参考我的把镜像推送到阿里云这一篇,来试着把镜像推送到阿里云
如果你不想推送,那么这个镜像位置在
/var/lib/docker
,你可以选择清理掉,dcoker images
,docker rmi
如果你下载了一个镜像,报错了或者你想查看一些构建逻辑,使用
docker history
,可以查看镜像的变更历史
项目中编写一个 dockerfile 的思路如下
- 基于一个空的镜像
- 下载需要的环境 ADD
- 执行环境变量的配置 ENV
- 执行一些Linux命令 RUN
- 日志 CMD
- 端口暴露 EXPOSE
- 挂载数据卷 VOLUMES
以构建java项目为例子
我们基于 Ubuntu 镜像构建一个新的镜像,运行一个 java 项目
我们在本地电脑上的java项目,我希望把它搞到VMWare上,应该怎么搞,就使用VMWare Tools 共享文件,或者SSH也可以,我个人习惯 Xftp
先把我们的 Java 项目进行打包,这里我以一个我之前写的 JavaFx 为例子,因为我在写这篇文章的时候 Linux 上的 tomcat 有点问题,我还没搞好,所以就用这种简单一些的来演示

传输到你的虚拟机中,然后我们开始编写Dockerfile,由于这个就是个普通的JavaFx程序,没啥好多说的
1 | # 选择基于 Alpine Linux 的 OpenJDK 21 运行时镜像 |
FROM openjdk:21-jre-alpine
:指定基础镜像为LABEL
:用于添加镜像的元数据,如维护者信息和镜像描述,方便管理和识别镜像。WORKDIR /app
:在镜像内部创建/app
目录,并将其设置为后续指令的工作目录,这样在使用COPY
等指令时,路径会相对简洁明了。COPY json-parser-1.0-SNAPSHOT.jar /app/json-parser.jar
:将本地的json-parser-1.0-SNAPSHOT.jar
文件复制到镜像内的/app
目录,并可选择重命名(也可保持原名),方便后续命令引用。EXPOSE 8080
:声明容器在运行时会暴露 8080 端口,这只是一个声明,实际的端口映射需要在docker run
时通过-p
参数指定。如果项目不是 Web 应用,没有对外暴露端口的需求,可以省略这一步。CMD ["java", "-jar", "json-parser.jar"]
:定义容器启动时默认执行的命令,这里使用java -jar
命令来运行json-parser.jar
文件。当使用docker run
启动容器时,如果没有额外指定其他命令,就会执行此命令。
然后我们构建镜像,在包含 Dockerfile
和
json-parser-1.0-SNAPSHOT.jar
的目录下,执行以下命令来构建镜像:
1 | docker build -t json-parser-java21-app:1.0 . |
虽然我的不是,但是我还是在这里讲解一下
可以看到我在上面开放了 8080 端口,因为我把这个当成 Web 应用来讲了,那么运行容器就会有所不同
如果是 Web 应用:
1
docker run -p 8080:8080 json-parser-java21-app:1.0
如果不是 Web 应用:
1
docker run json-parser-java21-app:1.0
直接运行容器,查看容器内项目的输出日志等信息。
查看一下
1 | docker imgaes |
如果你的 Java 项目在启动时需要传递额外参数,比如设置 JVM 内存参数、指定配置文件,可以在Dockerfile中额外指定
如果你从Ubuntu基础镜像来开始构建一个新镜像,然后运行一个java项目,可以参考之前我写的 dockerfile
1 | # 基于Ubuntu 22.04基础镜像,你也可以根据需求选择其他版本,如ubuntu:20.04 |
其中 JDK 安装的部分我详细说明一下
它通过一系列 RUN
命令来完成 JDK 21 的安装:
apt-get update
更新软件包列表。apt-get install -y wget
安装wget
工具,用于下载 JDK 安装包。wget -O /tmp/openjdk-21_linux-x64_bin.tar.gz https://download.java.net/java/GA/jdk21/...
从 Java 官方网站下载 JDK 21 的二进制压缩包到/tmp
目录。mkdir -p $JAVA_DIR
创建 JDK 安装目录。tar -xf /tmp/openjdk-21_linux-x64_bin.tar.gz -C $JAVA_DIR --strip-components=1
解压下载的压缩包到指定的 JDK 安装目录,并去掉一层目录结构。rm /tmp/openjdk-21_linux-x64_bin.tar.gz
删除下载的压缩包,节省镜像空间。apt-get remove -y wget
和apt-get clean
移除wget
软件包并清理软件包缓存,进一步减小镜像体积。rm -rf /var/lib/apt/lists/*
删除软件包列表缓存文件。
只不过,openjdk:21-jre-alpine
帮我们把上面这些步骤全部做完了