mirror of
https://github.com/yeasy/docker_practice.git
synced 2026-03-10 11:54:37 +00:00
Release v1.5.0: Restructure chapters and update for Docker v30.x
This commit is contained in:
13
04_image/README.md
Normal file
13
04_image/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# 使用 Docker 镜像
|
||||
|
||||
在之前的介绍中,我们知道镜像是 Docker 的三大组件之一。
|
||||
|
||||
Docker 运行容器前需要本地存在对应的镜像,如果本地不存在该镜像,Docker 会从镜像仓库下载该镜像。
|
||||
|
||||
本章将介绍更多关于镜像的内容,包括:
|
||||
|
||||
* 从仓库获取镜像;
|
||||
|
||||
* 管理本地主机上的镜像;
|
||||
|
||||
* 介绍镜像实现的基本原理。
|
||||
BIN
04_image/_images/images-create-nginx-docker.png
Normal file
BIN
04_image/_images/images-create-nginx-docker.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
BIN
04_image/_images/images-mac-example-nginx.png
Normal file
BIN
04_image/_images/images-mac-example-nginx.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
238
04_image/build.md
Normal file
238
04_image/build.md
Normal file
@@ -0,0 +1,238 @@
|
||||
# 使用 Dockerfile 定制镜像
|
||||
|
||||
从刚才的 `docker commit` 的学习中,我们可以了解到,镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。
|
||||
|
||||
Dockerfile 是一个文本文件,其内包含了一条条的 **指令(Instruction)**,每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
|
||||
|
||||
## 使用 docker init 快速创建(推荐)
|
||||
|
||||
Docker 提供了 `docker init` 命令,可以根据项目类型自动生成 Dockerfile、.dockerignore 和 compose.yaml 文件:
|
||||
|
||||
```bash
|
||||
$ docker init
|
||||
```
|
||||
|
||||
该命令会交互式地询问项目类型(如 Go、Node.js、Python、Rust 等),并生成符合最佳实践的配置文件。对于新项目,这是推荐的起步方式。
|
||||
|
||||
## 手动创建 Dockerfile
|
||||
|
||||
还以之前定制 `nginx` 镜像为例,这次我们使用 Dockerfile 来定制。
|
||||
|
||||
在一个空白目录中,建立一个文本文件,并命名为 `Dockerfile`:
|
||||
|
||||
```bash
|
||||
$ mkdir mynginx
|
||||
$ cd mynginx
|
||||
$ touch Dockerfile
|
||||
```
|
||||
|
||||
其内容为:
|
||||
|
||||
```docker
|
||||
FROM nginx
|
||||
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
|
||||
```
|
||||
|
||||
这个 Dockerfile 很简单,一共就两行。涉及到了两条指令,`FROM` 和 `RUN`。
|
||||
|
||||
## FROM 指定基础镜像
|
||||
|
||||
所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。就像我们之前运行了一个 `nginx` 镜像的容器,再进行修改一样,基础镜像是必须指定的。而 `FROM` 就是指定 **基础镜像**,因此一个 `Dockerfile` 中 `FROM` 是必备的指令,并且必须是第一条指令。
|
||||
|
||||
在 [Docker Hub](https://hub.docker.com/search?q=&type=image&image_filter=official) 上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如 [`nginx`](https://hub.docker.com/_/nginx/)、[`redis`](https://hub.docker.com/_/redis/)、[`mongo`](https://hub.docker.com/_/mongo/)、[`mysql`](https://hub.docker.com/_/mysql/)、[`httpd`](https://hub.docker.com/_/httpd/)、[`php`](https://hub.docker.com/_/php/)、[`tomcat`](https://hub.docker.com/_/tomcat/) 等;也有一些方便开发、构建、运行各种语言应用的镜像,如 [`node`](https://hub.docker.com/_/node)、[`openjdk`](https://hub.docker.com/_/openjdk/)、[`python`](https://hub.docker.com/_/python/)、[`ruby`](https://hub.docker.com/_/ruby/)、[`golang`](https://hub.docker.com/_/golang/) 等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。
|
||||
|
||||
如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如 [`ubuntu`](https://hub.docker.com/_/ubuntu/)、[`debian`](https://hub.docker.com/_/debian/)、[`centos`](https://hub.docker.com/_/centos/)、[`fedora`](https://hub.docker.com/_/fedora/)、[`alpine`](https://hub.docker.com/_/alpine/) 等,这些操作系统的软件库为我们提供了更广阔的扩展空间。
|
||||
|
||||
除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 `scratch`。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。
|
||||
|
||||
```docker
|
||||
FROM scratch
|
||||
...
|
||||
```
|
||||
|
||||
如果你以 `scratch` 为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。
|
||||
|
||||
不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接 `FROM scratch` 会让镜像体积更加小巧。使用 [Go 语言](https://golang.google.cn/) 开发的应用很多会使用这种方式来制作镜像,这也是有人认为 Go 是特别适合容器微服务架构的语言的原因之一。
|
||||
|
||||
## RUN 执行命令
|
||||
|
||||
`RUN` 指令是用来执行命令行命令的。由于命令行的强大能力,`RUN` 指令在定制镜像时是最常用的指令之一。其格式有两种:
|
||||
|
||||
* *shell* 格式:`RUN <命令>`,就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的 `RUN` 指令就是这种格式。
|
||||
|
||||
```docker
|
||||
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
|
||||
```
|
||||
|
||||
* *exec* 格式:`RUN ["可执行文件", "参数1", "参数2"]`,这更像是函数调用中的格式。
|
||||
|
||||
既然 `RUN` 就像 Shell 脚本一样可以执行命令,那么我们是否就可以像 Shell 脚本一样把每个命令对应一个 RUN 呢?比如这样:
|
||||
|
||||
```docker
|
||||
FROM debian:bookworm
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y gcc libc6-dev make wget
|
||||
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-7.2.4.tar.gz"
|
||||
RUN mkdir -p /usr/src/redis
|
||||
RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
|
||||
RUN make -C /usr/src/redis
|
||||
RUN make -C /usr/src/redis install
|
||||
```
|
||||
|
||||
之前说过,Dockerfile 中每一个指令都会建立一层,`RUN` 也不例外。每一个 `RUN` 的行为,就和刚才我们手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后,`commit` 这一层的修改,构成新的镜像。
|
||||
|
||||
而上面的这种写法,创建了 7 层镜像。这是完全没有意义的,而且很多运行时不需要的东西,都被装进了镜像里,比如编译环境、更新的软件包等等。结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。
|
||||
这是很多初学 Docker 的人常犯的一个错误。
|
||||
|
||||
*Union FS 是有最大层数限制的,比如 AUFS,曾经是最大不得超过 42 层,现在是不得超过 127 层。*
|
||||
|
||||
上面的 `Dockerfile` 正确的写法应该是这样:
|
||||
|
||||
```docker
|
||||
FROM debian:bookworm
|
||||
|
||||
RUN set -x; buildDeps='gcc libc6-dev make wget' \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y $buildDeps \
|
||||
&& wget -O redis.tar.gz "http://download.redis.io/releases/redis-7.2.4.tar.gz" \
|
||||
&& mkdir -p /usr/src/redis \
|
||||
&& tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
|
||||
&& make -C /usr/src/redis \
|
||||
&& make -C /usr/src/redis install \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& rm redis.tar.gz \
|
||||
&& rm -r /usr/src/redis \
|
||||
&& apt-get purge -y --auto-remove $buildDeps
|
||||
```
|
||||
|
||||
首先,之前所有的命令只有一个目的,就是编译、安装 redis 可执行文件。因此没有必要建立很多层,这只是一层的事情。因此,这里没有使用很多个 `RUN` 一一对应不同的命令,而是仅仅使用一个 `RUN` 指令,并使用 `&&` 将各个所需命令串联起来。将之前的 7 层,简化为了 1 层。在撰写 Dockerfile 的时候,要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建。
|
||||
|
||||
并且,这里为了格式化还进行了换行。Dockerfile 支持 Shell 类的行尾添加 `\` 的命令换行方式,以及行首 `#` 进行注释的格式。良好的格式,比如换行、缩进、注释等,会让维护、排障更为容易,这是一个比较好的习惯。
|
||||
|
||||
此外,还可以看到这一组命令的最后添加了清理工作的命令,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了 `apt` 缓存文件。这是很重要的一步,我们之前说过,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。
|
||||
|
||||
很多人初学 Docker 制作出了很臃肿的镜像的原因之一,就是忘记了每一层构建的最后一定要清理掉无关文件。
|
||||
|
||||
## 构建镜像
|
||||
|
||||
好了,让我们再回到之前定制的 nginx 镜像的 Dockerfile 来。现在我们明白了这个 Dockerfile 的内容,那么让我们来构建这个镜像吧。
|
||||
|
||||
在 `Dockerfile` 文件所在目录执行:
|
||||
|
||||
```bash
|
||||
$ docker build -t nginx:v3 .
|
||||
Sending build context to Docker daemon 2.048 kB
|
||||
Step 1 : FROM nginx
|
||||
---> e43d811ce2f4
|
||||
Step 2 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
|
||||
---> Running in 9cdc27646c7b
|
||||
---> 44aa4490ce2c
|
||||
Removing intermediate container 9cdc27646c7b
|
||||
Successfully built 44aa4490ce2c
|
||||
```
|
||||
|
||||
从命令的输出结果中,我们可以清晰的看到镜像的构建过程。在 `Step 2` 中,如同我们之前所说的那样,`RUN` 指令启动了一个容器 `9cdc27646c7b`,执行了所要求的命令,并最后提交了这一层 `44aa4490ce2c`,随后删除了所用到的这个容器 `9cdc27646c7b`。
|
||||
|
||||
这里我们使用了 `docker build` 命令进行镜像构建。其格式为:
|
||||
|
||||
```bash
|
||||
docker build [选项] <上下文路径/URL/->
|
||||
```
|
||||
|
||||
在这里我们指定了最终镜像的名称 `-t nginx:v3`,构建成功后,我们可以像之前运行 `nginx:v2` 那样来运行这个镜像,其结果会和 `nginx:v2` 一样。
|
||||
|
||||
## 镜像构建上下文(Context)
|
||||
|
||||
如果注意,会看到 `docker build` 命令最后有一个 `.`。`.` 表示当前目录,而 `Dockerfile` 就在当前目录,因此不少初学者以为这个路径是在指定 `Dockerfile` 所在路径,这么理解其实是不准确的。如果对应上面的命令格式,你可能会发现,这是在指定 **上下文路径**。那么什么是上下文呢?
|
||||
|
||||
首先我们要理解 `docker build` 的工作原理。Docker 在运行时分为 Docker 引擎(也就是服务端守护进程)和客户端工具。Docker 的引擎提供了一组 REST API,被称为 [Docker Remote API](https://docs.docker.com/develop/sdk/),而如 `docker` 命令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能。因此,虽然表面上我们好像是在本机执行各种 `docker` 功能,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。也因为这种 C/S 设计,让我们操作远程服务器的 Docker 引擎变得轻而易举。
|
||||
|
||||
当我们进行镜像构建的时候,并非所有定制都会通过 `RUN` 指令完成,经常会需要将一些本地文件复制进镜像,比如通过 `COPY` 指令、`ADD` 指令等。而 `docker build` 命令构建镜像,其实并非在本地构建,而是在服务端,也就是 Docker 引擎中构建的。那么在这种客户端/服务端的架构中,如何才能让服务端获得本地文件呢?
|
||||
|
||||
这就引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径,`docker build` 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。
|
||||
|
||||
如果在 `Dockerfile` 中这么写:
|
||||
|
||||
```docker
|
||||
COPY ./package.json /app/
|
||||
```
|
||||
|
||||
这并不是要复制执行 `docker build` 命令所在的目录下的 `package.json`,也不是复制 `Dockerfile` 所在目录下的 `package.json`,而是复制 **上下文(context)** 目录下的 `package.json`。
|
||||
|
||||
因此,`COPY` 这类指令中的源文件的路径都是*相对路径*。这也是初学者经常会问的为什么 `COPY ../package.json /app` 或者 `COPY /opt/xxxx /app` 无法工作的原因,因为这些路径已经超出了上下文的范围,Docker 引擎无法获得这些位置的文件。如果真的需要那些文件,应该将它们复制到上下文目录中去。
|
||||
|
||||
现在就可以理解刚才的命令 `docker build -t nginx:v3 .` 中的这个 `.`,实际上是在指定上下文的目录,`docker build` 命令会将该目录下的内容打包交给 Docker 引擎以帮助构建镜像。
|
||||
|
||||
如果观察 `docker build` 输出,我们其实已经看到了这个发送上下文的过程:
|
||||
|
||||
```bash
|
||||
$ docker build -t nginx:v3 .
|
||||
Sending build context to Docker daemon 2.048 kB
|
||||
...
|
||||
```
|
||||
|
||||
理解构建上下文对于镜像构建是很重要的,避免犯一些不应该的错误。比如有些初学者在发现 `COPY /opt/xxxx /app` 不工作后,于是干脆将 `Dockerfile` 放到了硬盘根目录去构建,结果发现 `docker build` 执行后,在发送一个几十 GB 的东西,极为缓慢而且很容易构建失败。那是因为这种做法是在让 `docker build` 打包整个硬盘,这显然是使用错误。
|
||||
|
||||
一般来说,应该会将 `Dockerfile` 置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 `.gitignore` 一样的语法写一个 `.dockerignore`,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的。
|
||||
|
||||
那么为什么会有人误以为 `.` 是指定 `Dockerfile` 所在目录呢?这是因为在默认情况下,如果不额外指定 `Dockerfile` 的话,会将上下文目录下的名为 `Dockerfile` 的文件作为 Dockerfile。
|
||||
|
||||
这只是默认行为,实际上 `Dockerfile` 的文件名并不要求必须为 `Dockerfile`,而且并不要求必须位于上下文目录中,比如可以用 `-f ../Dockerfile.php` 参数指定某个文件作为 `Dockerfile`。
|
||||
|
||||
当然,一般大家习惯性的会使用默认的文件名 `Dockerfile`,以及会将其置于镜像构建上下文目录中。
|
||||
|
||||
## 其它 `docker build` 的用法
|
||||
|
||||
### 直接用 Git repo 进行构建
|
||||
|
||||
或许你已经注意到了,`docker build` 还支持从 URL 构建,比如可以直接从 Git repo 中构建:
|
||||
|
||||
```bash
|
||||
# $env:DOCKER_BUILDKIT=0
|
||||
# export DOCKER_BUILDKIT=0
|
||||
|
||||
$ docker build -t hello-world https://github.com/docker-library/hello-world.git#master:amd64/hello-world
|
||||
|
||||
Step 1/3 : FROM scratch
|
||||
--->
|
||||
Step 2/3 : COPY hello /
|
||||
---> ac779757d46e
|
||||
Step 3/3 : CMD ["/hello"]
|
||||
---> Running in d2a513a760ed
|
||||
Removing intermediate container d2a513a760ed
|
||||
---> 038ad4142d2b
|
||||
Successfully built 038ad4142d2b
|
||||
```
|
||||
|
||||
这行命令指定了构建所需的 Git repo,并且指定分支为 `master`,构建目录为 `/amd64/hello-world/`,然后 Docker 就会自己去 `git clone` 这个项目、切换到指定分支、并进入到指定目录后开始构建。
|
||||
|
||||
### 用给定的 tar 压缩包构建
|
||||
|
||||
```bash
|
||||
$ docker build http://server/context.tar.gz
|
||||
```
|
||||
|
||||
如果所给出的 URL 不是个 Git repo,而是个 `tar` 压缩包,那么 Docker 引擎会下载这个包,并自动解压缩,以其作为上下文,开始构建。
|
||||
|
||||
### 从标准输入中读取 Dockerfile 进行构建
|
||||
|
||||
```bash
|
||||
docker build - < Dockerfile
|
||||
```
|
||||
|
||||
或
|
||||
|
||||
```bash
|
||||
cat Dockerfile | docker build -
|
||||
```
|
||||
|
||||
如果标准输入传入的是文本文件,则将其视为 `Dockerfile`,并开始构建。这种形式由于直接从标准输入中读取 Dockerfile 的内容,它没有上下文,因此不可以像其他方法那样可以将本地文件 `COPY` 进镜像之类的事情。
|
||||
|
||||
### 从标准输入中读取上下文压缩包进行构建
|
||||
|
||||
```bash
|
||||
$ docker build - < context.tar.gz
|
||||
```
|
||||
|
||||
如果发现标准输入的文件格式是 `gzip`、`bzip2` 以及 `xz` 的话,将会使其为上下文压缩包,直接将其展开,将里面视为上下文,并开始构建。
|
||||
131
04_image/commit.md
Normal file
131
04_image/commit.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# 利用 commit 理解镜像构成
|
||||
|
||||
> 注意:如果您是初学者,您可以暂时跳过后面的内容,直接学习 [容器](../05_container/) 一节。
|
||||
|
||||
注意: `docker commit` 命令除了学习之外,还有一些特殊的应用场合,比如被入侵后保存现场等。但是,不要使用 `docker commit` 定制镜像,定制镜像应该使用 `Dockerfile` 来完成。如果你想要定制镜像请查看下一小节。
|
||||
|
||||
镜像是容器的基础,每次执行 `docker run` 的时候都会指定哪个镜像作为容器运行的基础。在之前的例子中,我们所使用的都是来自于 Docker Hub 的镜像。直接使用这些镜像是可以满足一定的需求,而当这些镜像无法直接满足需求时,我们就需要定制这些镜像。接下来的几节就将讲解如何定制镜像。
|
||||
|
||||
回顾一下之前我们学到的知识,镜像是多层存储,每一层是在前一层的基础上进行的修改;而容器同样也是多层存储,是在以镜像为基础层,在其基础上加一层作为容器运行时的存储层。
|
||||
|
||||
现在让我们以定制一个 Web 服务器为例子,来讲解镜像是如何构建的。
|
||||
|
||||
```bash
|
||||
$ docker run --name webserver -d -p 80:80 nginx
|
||||
```
|
||||
|
||||
这条命令会用 `nginx` 镜像启动一个容器,命名为 `webserver`,并且映射了 80 端口,这样我们可以用浏览器去访问这个 `nginx` 服务器。
|
||||
|
||||
如果是在本机运行的 Docker,那么可以直接访问:`http://localhost` ,如果是在虚拟机、云服务器上安装的 Docker,则需要将 `localhost` 换为虚拟机地址或者实际云服务器地址。
|
||||
|
||||
直接用浏览器访问的话,我们会看到默认的 Nginx 欢迎页面。
|
||||
|
||||

|
||||
|
||||
现在,假设我们非常不喜欢这个欢迎页面,我们希望改成欢迎 Docker 的文字,我们可以使用 `docker exec` 命令进入容器,修改其内容。
|
||||
|
||||
```bash
|
||||
$ docker exec -it webserver bash
|
||||
root@3729b97e8226:/# echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
|
||||
root@3729b97e8226:/# exit
|
||||
exit
|
||||
```
|
||||
|
||||
我们以交互式终端方式进入 `webserver` 容器,并执行了 `bash` 命令,也就是获得一个可操作的 Shell。
|
||||
|
||||
然后,我们用 `<h1>Hello, Docker!</h1>` 覆盖了 `/usr/share/nginx/html/index.html` 的内容。
|
||||
|
||||
现在我们再刷新浏览器的话,会发现内容被改变了。
|
||||
|
||||

|
||||
|
||||
我们修改了容器的文件,也就是改动了容器的存储层。我们可以通过 `docker diff` 命令看到具体的改动。
|
||||
|
||||
```bash
|
||||
$ docker diff webserver
|
||||
C /root
|
||||
A /root/.bash_history
|
||||
C /run
|
||||
C /usr
|
||||
C /usr/share
|
||||
C /usr/share/nginx
|
||||
C /usr/share/nginx/html
|
||||
C /usr/share/nginx/html/index.html
|
||||
C /var
|
||||
C /var/cache
|
||||
C /var/cache/nginx
|
||||
A /var/cache/nginx/client_temp
|
||||
A /var/cache/nginx/fastcgi_temp
|
||||
A /var/cache/nginx/proxy_temp
|
||||
A /var/cache/nginx/scgi_temp
|
||||
A /var/cache/nginx/uwsgi_temp
|
||||
```
|
||||
|
||||
现在我们定制好了变化,我们希望能将其保存下来形成镜像。
|
||||
|
||||
要知道,当我们运行一个容器的时候(如果不使用卷的话),我们做的任何文件修改都会被记录于容器存储层里。而 Docker 提供了一个 `docker commit` 命令,可以将容器的存储层保存下来成为镜像。换句话说,就是在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。以后我们运行这个新镜像的时候,就会拥有原有容器最后的文件变化。
|
||||
|
||||
`docker commit` 的语法格式为:
|
||||
|
||||
```bash
|
||||
docker commit [选项] <容器ID或容器名> [<仓库名>[:<标签>]]
|
||||
```
|
||||
|
||||
我们可以用下面的命令将容器保存为镜像:
|
||||
|
||||
```bash
|
||||
$ docker commit \
|
||||
--author "Tao Wang <twang2218@gmail.com>" \
|
||||
--message "修改了默认网页" \
|
||||
webserver \
|
||||
nginx:v2
|
||||
sha256:07e33465974800ce65751acc279adc6ed2dc5ed4e0838f8b86f0c87aa1795214
|
||||
```
|
||||
|
||||
其中 `--author` 是指定修改的作者,而 `--message` 则是记录本次修改的内容。这点和 `git` 版本控制相似,不过这里这些信息可以省略留空。
|
||||
|
||||
我们可以在 `docker image ls` 中看到这个新定制的镜像:
|
||||
|
||||
```bash
|
||||
$ docker image ls nginx
|
||||
REPOSITORY TAG IMAGE ID CREATED SIZE
|
||||
nginx v2 07e334659748 9 seconds ago 181.5 MB
|
||||
nginx 1.27 05a60462f8ba 12 days ago 181.5 MB
|
||||
nginx latest e43d811ce2f4 4 weeks ago 181.5 MB
|
||||
```
|
||||
|
||||
我们还可以用 `docker history` 具体查看镜像内的历史记录,如果比较 `nginx:latest` 的历史记录,我们会发现新增了我们刚刚提交的这一层。
|
||||
|
||||
```bash
|
||||
$ docker history nginx:v2
|
||||
IMAGE CREATED CREATED BY SIZE COMMENT
|
||||
07e334659748 54 seconds ago nginx -g daemon off; 95 B 修改了默认网页
|
||||
e43d811ce2f4 4 weeks ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon 0 B
|
||||
<missing> 4 weeks ago /bin/sh -c #(nop) EXPOSE 443/tcp 80/tcp 0 B
|
||||
<missing> 4 weeks ago /bin/sh -c ln -sf /dev/stdout /var/log/nginx/ 22 B
|
||||
<missing> 4 weeks ago /bin/sh -c apt-key adv --keyserver hkp://pgp. 58.46 MB
|
||||
<missing> 4 weeks ago /bin/sh -c #(nop) ENV NGINX_VERSION=1.27.0-1 0 B
|
||||
<missing> 4 weeks ago /bin/sh -c #(nop) MAINTAINER NGINX Docker Ma 0 B
|
||||
<missing> 4 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0 B
|
||||
<missing> 4 weeks ago /bin/sh -c #(nop) ADD file:23aa4f893e3288698c 123 MB
|
||||
```
|
||||
|
||||
新的镜像定制好后,我们可以来运行这个镜像。
|
||||
|
||||
```bash
|
||||
docker run --name web2 -d -p 81:80 nginx:v2
|
||||
```
|
||||
|
||||
这里我们命名为新的服务为 `web2`,并且映射到 `81` 端口。访问 `http://localhost:81` 看到结果,其内容应该和之前修改后的 `webserver` 一样。
|
||||
|
||||
至此,我们第一次完成了定制镜像,使用的是 `docker commit` 命令,手动操作给旧的镜像添加了新的一层,形成新的镜像,对镜像多层存储应该有了更直观的感觉。
|
||||
|
||||
## 慎用 `docker commit`
|
||||
|
||||
使用 `docker commit` 命令虽然可以比较直观的帮助理解镜像分层存储的概念,但是实际环境中并不会这样使用。
|
||||
|
||||
首先,如果仔细观察之前的 `docker diff webserver` 的结果,你会发现除了真正想要修改的 `/usr/share/nginx/html/index.html` 文件外,由于命令的执行,还有很多文件被改动或添加了。这还仅仅是最简单的操作,如果是安装软件包、编译构建,那会有大量的无关内容被添加进来,将会导致镜像极为臃肿。
|
||||
|
||||
此外,使用 `docker commit` 意味着所有对镜像的操作都是黑箱操作,生成的镜像也被称为 **黑箱镜像**,换句话说,就是除了制作镜像的人知道执行过什么命令、怎么生成的镜像,别人根本无从得知。而且,即使是这个制作镜像的人,过一段时间后也无法记清具体的操作。这种黑箱镜像的维护工作是非常痛苦的。
|
||||
|
||||
而且,回顾之前提及的镜像所使用的分层存储的概念,除当前层外,之前的每一层都是不会发生改变的,换句话说,任何修改的结果仅仅是在当前层进行标记、添加、修改,而不会改动上一层。如果使用 `docker commit` 制作镜像,以及后期修改的话,每一次修改都会让镜像更加臃肿一次,所删除的上一层的东西并不会丢失,会一直如影随形的跟着这个镜像,即使根本无法访问到。这会让镜像更加臃肿。
|
||||
16
04_image/demo/buildkit/Dockerfile
Normal file
16
04_image/demo/buildkit/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM node:alpine as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json /app/
|
||||
|
||||
RUN npm i --registry=https://registry.npmmirror.com \
|
||||
&& rm -rf ~/.npm
|
||||
|
||||
COPY src /app/src
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY --from=builder /app/dist /app/dist
|
||||
37
04_image/demo/buildkit/Dockerfile.buildkit
Normal file
37
04_image/demo/buildkit/Dockerfile.buildkit
Normal file
@@ -0,0 +1,37 @@
|
||||
# syntax = docker/dockerfile:experimental
|
||||
|
||||
FROM node:alpine as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json /app/
|
||||
|
||||
RUN --mount=type=cache,target=/app/node_modules,id=my_app_npm_module,sharing=locked \
|
||||
--mount=type=cache,target=/root/.npm,id=npm_cache \
|
||||
npm i --registry=https://registry.npmmirror.com
|
||||
|
||||
COPY src /app/src
|
||||
|
||||
RUN --mount=type=cache,target=/app/node_modules,id=my_app_npm_module,sharing=locked \
|
||||
# --mount=type=cache,target=/app/dist,id=my_app_dist,sharing=locked \
|
||||
npm run build
|
||||
|
||||
FROM nginx:alpine
|
||||
|
||||
# COPY --from=builder /app/dist /app/dist
|
||||
|
||||
# 为了更直观的说明 from 和 source 指令,这里使用 RUN 指令
|
||||
RUN --mount=type=cache,target=/tmp/dist,from=builder,source=/app/dist \
|
||||
# --mount=type=cache,target/tmp/dist,from=my_app_dist,sharing=locked \
|
||||
mkdir -p /app/dist && cp -r /tmp/dist/* /app/dist
|
||||
|
||||
RUN --mount=type=bind,from=php:alpine,source=/usr/local/bin/docker-php-entrypoint,target=/docker-php-entrypoint \
|
||||
cat /docker-php-entrypoint
|
||||
|
||||
RUN --mount=type=tmpfs,target=/temp \
|
||||
mount | grep /temp
|
||||
|
||||
RUN --mount=type=secret,id=aws,target=/root/.aws/credentials \
|
||||
cat /root/.aws/credentials
|
||||
|
||||
# docker build -t test --secret id=aws,src=$PWD/aws.txt --progress=plain -f Dockerfile.buildkit .
|
||||
1
04_image/demo/buildkit/aws.txt
Normal file
1
04_image/demo/buildkit/aws.txt
Normal file
@@ -0,0 +1 @@
|
||||
awskey
|
||||
11
04_image/demo/buildkit/package.json
Normal file
11
04_image/demo/buildkit/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "my_app",
|
||||
"version": "19.6.0",
|
||||
"devDependencies": {
|
||||
"webpack": "*",
|
||||
"webpack-cli": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "mkdir -p $PWD/dist && cp -r src/* dist/"
|
||||
}
|
||||
}
|
||||
1
04_image/demo/buildkit/src/index.js
Normal file
1
04_image/demo/buildkit/src/index.js
Normal file
@@ -0,0 +1 @@
|
||||
console.log(1);
|
||||
5
04_image/demo/multi-arch/Dockerfile
Normal file
5
04_image/demo/multi-arch/Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
||||
FROM --platform=$TARGETPLATFORM alpine
|
||||
|
||||
RUN uname -a > /os.txt
|
||||
|
||||
CMD cat /os.txt
|
||||
1
04_image/demo/multistage-builds/.gitignore
vendored
Normal file
1
04_image/demo/multistage-builds/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
app
|
||||
21
04_image/demo/multistage-builds/Dockerfile
Normal file
21
04_image/demo/multistage-builds/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM golang:alpine as builder
|
||||
|
||||
RUN apk --no-cache add git
|
||||
|
||||
WORKDIR /go/src/github.com/go/helloworld/
|
||||
|
||||
RUN go get -d -v github.com/go-sql-driver/mysql
|
||||
|
||||
COPY app.go .
|
||||
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
|
||||
|
||||
FROM alpine:latest as prod
|
||||
|
||||
RUN apk --no-cache add ca-certificates
|
||||
|
||||
WORKDIR /root/
|
||||
|
||||
COPY --from=0 /go/src/github.com/go/helloworld/app .
|
||||
|
||||
CMD ["./app"]
|
||||
10
04_image/demo/multistage-builds/Dockerfile.build
Normal file
10
04_image/demo/multistage-builds/Dockerfile.build
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM golang:alpine
|
||||
|
||||
RUN apk --no-cache add git
|
||||
|
||||
WORKDIR /go/src/github.com/go/helloworld
|
||||
|
||||
COPY app.go .
|
||||
|
||||
RUN go get -d -v github.com/go-sql-driver/mysql \
|
||||
&& CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
|
||||
9
04_image/demo/multistage-builds/Dockerfile.copy
Normal file
9
04_image/demo/multistage-builds/Dockerfile.copy
Normal file
@@ -0,0 +1,9 @@
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk --no-cache add ca-certificates
|
||||
|
||||
WORKDIR /root/
|
||||
|
||||
COPY app .
|
||||
|
||||
CMD ["./app"]
|
||||
15
04_image/demo/multistage-builds/Dockerfile.one
Normal file
15
04_image/demo/multistage-builds/Dockerfile.one
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM golang:alpine
|
||||
|
||||
RUN apk --no-cache add git ca-certificates
|
||||
|
||||
WORKDIR /go/src/github.com/go/helloworld/
|
||||
|
||||
COPY app.go .
|
||||
|
||||
RUN go get -d -v github.com/go-sql-driver/mysql \
|
||||
&& CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . \
|
||||
&& cp /go/src/github.com/go/helloworld/app /root
|
||||
|
||||
WORKDIR /root/
|
||||
|
||||
CMD ["./app"]
|
||||
7
04_image/demo/multistage-builds/app.go
Normal file
7
04_image/demo/multistage-builds/app.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main(){
|
||||
fmt.Printf("Hello World!");
|
||||
}
|
||||
14
04_image/demo/multistage-builds/build.sh
Normal file
14
04_image/demo/multistage-builds/build.sh
Normal file
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo Building go/helloworld:build
|
||||
|
||||
docker build -t go/helloworld:build . -f Dockerfile.build
|
||||
|
||||
docker create --name extract go/helloworld:build
|
||||
docker cp extract:/go/src/github.com/go/helloworld/app ./app
|
||||
docker rm -f extract
|
||||
|
||||
echo Building go/helloworld:2
|
||||
|
||||
docker build --no-cache -t go/helloworld:2 . -f Dockerfile.copy
|
||||
rm ./app
|
||||
3
04_image/dockerfile/README.md
Normal file
3
04_image/dockerfile/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Dockerfile 指令详解
|
||||
|
||||
我们已经介绍了 `FROM`,`RUN`,还提及了 `COPY`, `ADD`,其实 `Dockerfile` 功能很强大,它提供了十多个指令。下面我们继续讲解其他的指令。
|
||||
221
04_image/dockerfile/add.md
Normal file
221
04_image/dockerfile/add.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# ADD 更高级的复制文件
|
||||
|
||||
## 基本语法
|
||||
|
||||
```docker
|
||||
ADD [选项] <源路径>... <目标路径>
|
||||
ADD [选项] ["<源路径>", ... "<目标路径>"]
|
||||
```
|
||||
|
||||
`ADD` 在 `COPY` 基础上增加了两个功能:
|
||||
1. 自动解压 tar 压缩包
|
||||
2. 支持从 URL 下载文件(不推荐)
|
||||
|
||||
---
|
||||
|
||||
## ADD vs COPY
|
||||
|
||||
| 特性 | COPY | ADD |
|
||||
|------|------|-----|
|
||||
| 复制本地文件 | ✅ | ✅ |
|
||||
| 自动解压 tar | ❌ | ✅ |
|
||||
| 支持 URL | ❌ | ✅(不推荐) |
|
||||
| 行为可预测性 | ✅ 高 | ⚠️ 低 |
|
||||
| 推荐程度 | ✅ **优先使用** | 仅解压场景 |
|
||||
|
||||
> 笔者建议:除非需要自动解压 tar 文件,否则始终使用 COPY。明确的行为比隐式的魔法更好。
|
||||
|
||||
---
|
||||
|
||||
## 自动解压功能
|
||||
|
||||
### 基本用法
|
||||
|
||||
```docker
|
||||
# 自动解压 tar.gz 到目标目录
|
||||
ADD app.tar.gz /app/
|
||||
```
|
||||
|
||||
ADD 会识别并解压以下格式:
|
||||
- `.tar`
|
||||
- `.tar.gz` / `.tgz`
|
||||
- `.tar.bz2` / `.tbz2`
|
||||
- `.tar.xz` / `.txz`
|
||||
|
||||
### 实际应用
|
||||
|
||||
官方基础镜像通常使用 ADD 解压根文件系统:
|
||||
|
||||
```docker
|
||||
FROM scratch
|
||||
ADD ubuntu-noble-core-cloudimg-amd64-root.tar.gz /
|
||||
```
|
||||
|
||||
### 解压过程
|
||||
|
||||
```
|
||||
ADD app.tar.gz /app/
|
||||
│
|
||||
├─ 识别 .tar.gz 格式
|
||||
├─ 自动解压
|
||||
└─ 内容放入 /app/
|
||||
|
||||
app.tar.gz 包含: /app/ 目录结果:
|
||||
├── src/ ├── src/
|
||||
│ └── main.py │ └── main.py
|
||||
└── config.json └── config.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## URL 下载功能(不推荐)
|
||||
|
||||
### 基本用法
|
||||
|
||||
```docker
|
||||
# 从 URL 下载文件
|
||||
ADD https://example.com/app.zip /app/app.zip
|
||||
```
|
||||
|
||||
### 为什么不推荐
|
||||
|
||||
| 问题 | 说明 |
|
||||
|------|------|
|
||||
| 权限固定 | 下载的文件权限为 600,通常需要额外 RUN 修改 |
|
||||
| 不会解压 | URL 下载的压缩包不会自动解压 |
|
||||
| 缓存问题 | URL 内容变化时不会重新下载 |
|
||||
| 层数增加 | 需要额外 RUN 清理 |
|
||||
|
||||
### 推荐替代方案
|
||||
|
||||
```docker
|
||||
# ❌ 不推荐:使用 ADD 下载
|
||||
ADD https://example.com/app.tar.gz /tmp/
|
||||
RUN tar -xzf /tmp/app.tar.gz -C /app && rm /tmp/app.tar.gz
|
||||
|
||||
# ✅ 推荐:使用 RUN + curl
|
||||
RUN curl -fsSL https://example.com/app.tar.gz | tar -xz -C /app
|
||||
```
|
||||
|
||||
优势:
|
||||
- 一条 RUN 完成下载、解压、清理
|
||||
- 减少镜像层数
|
||||
- 更清晰的构建意图
|
||||
|
||||
---
|
||||
|
||||
## 修改文件所有者
|
||||
|
||||
```docker
|
||||
ADD --chown=node:node app.tar.gz /app/
|
||||
ADD --chown=1000:1000 files/ /app/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 何时使用 ADD
|
||||
|
||||
### ✅ 适合使用 ADD
|
||||
|
||||
```docker
|
||||
# 解压本地 tar 文件
|
||||
FROM scratch
|
||||
ADD rootfs.tar.gz /
|
||||
|
||||
# 解压应用包
|
||||
ADD dist.tar.gz /app/
|
||||
```
|
||||
|
||||
### ❌ 不适合使用 ADD
|
||||
|
||||
```docker
|
||||
# 复制普通文件(用 COPY)
|
||||
ADD package.json /app/ # ❌
|
||||
COPY package.json /app/ # ✅
|
||||
|
||||
# 下载文件(用 RUN + curl)
|
||||
ADD https://example.com/file / # ❌
|
||||
RUN curl -fsSL ... -o /file # ✅
|
||||
|
||||
# 需要保留 tar 不解压(用 COPY)
|
||||
ADD archive.tar.gz /archives/ # ❌ 会解压
|
||||
COPY archive.tar.gz /archives/ # ✅ 保持原样
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 缓存行为
|
||||
|
||||
ADD 可能导致构建缓存失效:
|
||||
|
||||
```docker
|
||||
# 如果 app.tar.gz 内容变化,此层及后续层都需重建
|
||||
ADD app.tar.gz /app/
|
||||
RUN npm install
|
||||
```
|
||||
|
||||
**优化建议**:
|
||||
|
||||
```docker
|
||||
# 先复制依赖文件
|
||||
COPY package*.json /app/
|
||||
RUN npm install
|
||||
|
||||
# 再添加应用代码
|
||||
ADD app.tar.gz /app/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 默认使用 COPY
|
||||
|
||||
```docker
|
||||
# ✅ 大多数场景使用 COPY
|
||||
COPY . /app/
|
||||
```
|
||||
|
||||
### 2. 仅在需要解压时使用 ADD
|
||||
|
||||
```docker
|
||||
# ✅ 自动解压场景
|
||||
ADD app.tar.gz /app/
|
||||
```
|
||||
|
||||
### 3. 不要用 ADD 下载文件
|
||||
|
||||
```docker
|
||||
# ❌ 避免
|
||||
ADD https://example.com/file.tar.gz /tmp/
|
||||
|
||||
# ✅ 推荐
|
||||
RUN curl -fsSL https://example.com/file.tar.gz | tar -xz -C /app
|
||||
```
|
||||
|
||||
### 4. 解压后清理
|
||||
|
||||
```docker
|
||||
# 如果需要控制解压过程
|
||||
COPY app.tar.gz /tmp/
|
||||
RUN tar -xzf /tmp/app.tar.gz -C /app && \
|
||||
rm /tmp/app.tar.gz
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 本章小结
|
||||
|
||||
| 场景 | 推荐指令 |
|
||||
|------|---------|
|
||||
| 复制普通文件 | `COPY` |
|
||||
| 复制目录 | `COPY` |
|
||||
| 自动解压 tar | `ADD` |
|
||||
| 从 URL 下载 | `RUN curl` |
|
||||
| 保持 tar 不解压 | `COPY` |
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- [COPY 复制文件](copy.md):基本复制操作
|
||||
- [多阶段构建](../multistage-builds.md):减少镜像体积
|
||||
- [最佳实践](../../15_appendix/best_practices.md):Dockerfile 编写指南
|
||||
238
04_image/dockerfile/arg.md
Normal file
238
04_image/dockerfile/arg.md
Normal file
@@ -0,0 +1,238 @@
|
||||
# ARG 构建参数
|
||||
|
||||
## 基本语法
|
||||
|
||||
```docker
|
||||
ARG <参数名>[=<默认值>]
|
||||
```
|
||||
|
||||
`ARG` 指令定义构建时的变量,可以在 `docker build` 时通过 `--build-arg` 传入。
|
||||
|
||||
---
|
||||
|
||||
## ARG vs ENV
|
||||
|
||||
| 特性 | ARG | ENV |
|
||||
|------|-----|-----|
|
||||
| **生效时间** | 仅构建时 | 构建时 + 运行时 |
|
||||
| **持久性** | 构建后消失 | 写入镜像 |
|
||||
| **覆盖方式** | `docker build --build-arg` | `docker run -e` |
|
||||
| **适用场景** | 构建参数(版本号等) | 应用配置 |
|
||||
| **可见性** | `docker history` 可见 | `docker inspect` 可见 |
|
||||
|
||||
```
|
||||
构建时 运行时
|
||||
├─ ARG VERSION=1.0 │ (ARG 已消失)
|
||||
├─ ENV APP_ENV=prod │ APP_ENV=prod(仍存在)
|
||||
└─ RUN echo $VERSION │
|
||||
```
|
||||
|
||||
> ⚠️ **安全提示**:不要用 ARG 传递密码等敏感信息,`docker history` 可以查看所有 ARG 值。
|
||||
|
||||
---
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 定义和使用
|
||||
|
||||
```docker
|
||||
# 定义有默认值的 ARG
|
||||
ARG NODE_VERSION=20
|
||||
|
||||
# 使用 ARG
|
||||
FROM node:${NODE_VERSION}-alpine
|
||||
RUN echo "Using Node.js $NODE_VERSION"
|
||||
```
|
||||
|
||||
### 构建时覆盖
|
||||
|
||||
```bash
|
||||
# 使用默认值
|
||||
$ docker build -t myapp .
|
||||
|
||||
# 覆盖默认值
|
||||
$ docker build --build-arg NODE_VERSION=18 -t myapp .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ARG 的作用域
|
||||
|
||||
### FROM 之前的 ARG
|
||||
|
||||
```docker
|
||||
# FROM 之前的 ARG 只能用于 FROM 指令
|
||||
ARG REGISTRY=docker.io
|
||||
ARG IMAGE_NAME=node
|
||||
|
||||
FROM ${REGISTRY}/${IMAGE_NAME}:20
|
||||
|
||||
# ❌ 这里无法使用上面的 ARG
|
||||
RUN echo $REGISTRY # 输出空
|
||||
```
|
||||
|
||||
### FROM 之后重新声明
|
||||
|
||||
```docker
|
||||
ARG NODE_VERSION=20
|
||||
|
||||
FROM node:${NODE_VERSION}-alpine
|
||||
|
||||
# 需要再次声明才能使用
|
||||
ARG NODE_VERSION
|
||||
RUN echo "Node version: $NODE_VERSION"
|
||||
```
|
||||
|
||||
### 多阶段构建中的 ARG
|
||||
|
||||
```docker
|
||||
ARG BASE_VERSION=alpine
|
||||
|
||||
FROM node:20-${BASE_VERSION} AS builder
|
||||
# 需要重新声明
|
||||
ARG NODE_VERSION=20
|
||||
RUN echo "Building with Node $NODE_VERSION"
|
||||
|
||||
FROM node:20-${BASE_VERSION}
|
||||
# 每个阶段都需要重新声明
|
||||
ARG NODE_VERSION=20
|
||||
RUN echo "Running with Node $NODE_VERSION"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见使用场景
|
||||
|
||||
### 1. 控制基础镜像版本
|
||||
|
||||
```docker
|
||||
ARG ALPINE_VERSION=3.19
|
||||
FROM alpine:${ALPINE_VERSION}
|
||||
```
|
||||
|
||||
```bash
|
||||
$ docker build --build-arg ALPINE_VERSION=3.18 .
|
||||
```
|
||||
|
||||
### 2. 设置软件版本
|
||||
|
||||
```docker
|
||||
ARG NGINX_VERSION=1.25.0
|
||||
|
||||
RUN curl -fsSL https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz | tar -xz
|
||||
```
|
||||
|
||||
### 3. 配置构建环境
|
||||
|
||||
```docker
|
||||
ARG BUILD_ENV=production
|
||||
ARG ENABLE_DEBUG=false
|
||||
|
||||
RUN if [ "$ENABLE_DEBUG" = "true" ]; then \
|
||||
npm install --include=dev; \
|
||||
else \
|
||||
npm install --production; \
|
||||
fi
|
||||
```
|
||||
|
||||
### 4. 配置私有仓库
|
||||
|
||||
```docker
|
||||
ARG NPM_TOKEN
|
||||
|
||||
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc && \
|
||||
npm install && \
|
||||
rm ~/.npmrc
|
||||
```
|
||||
|
||||
```bash
|
||||
# 构建时传入 token
|
||||
$ docker build --build-arg NPM_TOKEN=xxx .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 将 ARG 传递给 ENV
|
||||
|
||||
如果需要在运行时使用 ARG 的值:
|
||||
|
||||
```docker
|
||||
ARG VERSION=1.0.0
|
||||
|
||||
# 将 ARG 传递给 ENV
|
||||
ENV APP_VERSION=$VERSION
|
||||
|
||||
# 运行时可用
|
||||
CMD echo "App version: $APP_VERSION"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 预定义 ARG
|
||||
|
||||
Docker 提供了一些预定义的 ARG,无需声明即可使用:
|
||||
|
||||
| ARG | 说明 |
|
||||
|-----|------|
|
||||
| `HTTP_PROXY` | HTTP 代理 |
|
||||
| `HTTPS_PROXY` | HTTPS 代理 |
|
||||
| `NO_PROXY` | 不使用代理的地址 |
|
||||
| `FTP_PROXY` | FTP 代理 |
|
||||
|
||||
```bash
|
||||
# 构建时使用代理
|
||||
$ docker build --build-arg HTTP_PROXY=http://proxy:8080 .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 为 ARG 提供合理默认值
|
||||
|
||||
```docker
|
||||
# ✅ 好:有默认值
|
||||
ARG NODE_VERSION=20
|
||||
|
||||
# ⚠️ 需要每次传入
|
||||
ARG NODE_VERSION
|
||||
```
|
||||
|
||||
### 2. 不要用 ARG 存储敏感信息
|
||||
|
||||
```docker
|
||||
# ❌ 错误:密码会被记录在镜像历史中
|
||||
ARG DB_PASSWORD
|
||||
RUN echo "password=$DB_PASSWORD" > /app/.env
|
||||
|
||||
# ✅ 正确:使用 secrets 或运行时环境变量
|
||||
```
|
||||
|
||||
### 3. 使用 ARG 提高构建灵活性
|
||||
|
||||
```docker
|
||||
ARG BASE_IMAGE=python:3.12-slim
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
# 可以构建不同基础镜像的版本
|
||||
# docker build --build-arg BASE_IMAGE=python:3.11-alpine .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 本章小结
|
||||
|
||||
| 要点 | 说明 |
|
||||
|------|------|
|
||||
| **作用** | 定义构建时变量 |
|
||||
| **语法** | `ARG NAME=value` |
|
||||
| **覆盖** | `docker build --build-arg NAME=value` |
|
||||
| **作用域** | FROM 之后需要重新声明 |
|
||||
| **vs ENV** | ARG 仅构建时,ENV 构建+运行时 |
|
||||
| **安全** | 不要存储敏感信息 |
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- [ENV 设置环境变量](env.md):运行时环境变量
|
||||
- [FROM 指令](../../04_image/build.md):基础镜像指定
|
||||
- [多阶段构建](../multistage-builds.md):复杂构建场景
|
||||
268
04_image/dockerfile/cmd.md
Normal file
268
04_image/dockerfile/cmd.md
Normal file
@@ -0,0 +1,268 @@
|
||||
# CMD 容器启动命令
|
||||
|
||||
## 什么是 CMD
|
||||
|
||||
`CMD` 指令用于指定容器启动时默认执行的命令。它定义了容器的"主进程"。
|
||||
|
||||
> **核心概念**:容器的生命周期 = 主进程的生命周期。CMD 指定的命令就是这个主进程。
|
||||
|
||||
---
|
||||
|
||||
## 语法格式
|
||||
|
||||
CMD 有三种格式:
|
||||
|
||||
| 格式 | 语法 | 推荐程度 |
|
||||
|------|------|---------|
|
||||
| **exec 格式** | `CMD ["可执行文件", "参数1", "参数2"]` | ✅ **推荐** |
|
||||
| **shell 格式** | `CMD 命令 参数1 参数2` | ⚠️ 简单场景 |
|
||||
| **参数格式** | `CMD ["参数1", "参数2"]` | 配合 ENTRYPOINT |
|
||||
|
||||
### exec 格式(推荐)
|
||||
|
||||
```docker
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
CMD ["python", "app.py"]
|
||||
CMD ["node", "server.js"]
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- 直接执行指定程序,是容器的 PID 1
|
||||
- 正确接收信号(如 SIGTERM)
|
||||
- 无需 shell 解析
|
||||
|
||||
### shell 格式
|
||||
|
||||
```docker
|
||||
CMD echo "Hello World"
|
||||
CMD nginx -g "daemon off;"
|
||||
```
|
||||
|
||||
**实际执行**:会被包装为 `sh -c`
|
||||
|
||||
```docker
|
||||
# 你写的
|
||||
CMD echo $HOME
|
||||
|
||||
# 实际执行的
|
||||
CMD ["sh", "-c", "echo $HOME"]
|
||||
```
|
||||
|
||||
**优点**:可以使用环境变量、管道等 shell 特性
|
||||
**缺点**:主进程是 sh,信号无法正确传递给应用
|
||||
|
||||
---
|
||||
|
||||
## exec 格式 vs shell 格式
|
||||
|
||||
| 特性 | exec 格式 | shell 格式 |
|
||||
|------|----------|-----------|
|
||||
| 主进程 | 指定的程序 | `/bin/sh` |
|
||||
| 信号传递 | ✅ 正确 | ❌ 无法传递 |
|
||||
| 环境变量 | ❌ 需要 shell 包装 | ✅ 自动解析 |
|
||||
| 推荐使用 | ✅ 大多数场景 | 需要 shell 特性时 |
|
||||
|
||||
### 信号传递问题示例
|
||||
|
||||
```docker
|
||||
# ❌ shell 格式:docker stop 会超时
|
||||
CMD node server.js
|
||||
# 实际是 sh -c "node server.js"
|
||||
# SIGTERM 发给 sh,不会传递给 node
|
||||
|
||||
# ✅ exec 格式:docker stop 正常工作
|
||||
CMD ["node", "server.js"]
|
||||
# SIGTERM 直接发给 node
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 运行时覆盖 CMD
|
||||
|
||||
`docker run` 后的命令会覆盖 Dockerfile 中的 CMD:
|
||||
|
||||
```bash
|
||||
# ubuntu 默认 CMD 是 /bin/bash
|
||||
$ docker run -it ubuntu # 进入 bash
|
||||
$ docker run ubuntu cat /etc/os-release # 覆盖为 cat 命令
|
||||
```
|
||||
|
||||
```
|
||||
Dockerfile: docker run 命令:
|
||||
CMD ["/bin/bash"] + cat /etc/os-release
|
||||
│ │
|
||||
└───────► 被覆盖 ◄───────┘
|
||||
↓
|
||||
执行: cat /etc/os-release
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 经典错误:容器立即退出
|
||||
|
||||
### 错误示例
|
||||
|
||||
```docker
|
||||
# ❌ 容器启动后立即退出
|
||||
CMD service nginx start
|
||||
```
|
||||
|
||||
### 原因分析
|
||||
|
||||
```
|
||||
1. CMD service nginx start
|
||||
↓ 被转换为
|
||||
2. CMD ["sh", "-c", "service nginx start"]
|
||||
↓
|
||||
3. sh 启动,执行 service 命令
|
||||
↓
|
||||
4. service 命令将 nginx 放到后台
|
||||
↓
|
||||
5. service 命令结束,sh 退出
|
||||
↓
|
||||
6. 容器主进程(sh)退出 → 容器停止
|
||||
```
|
||||
|
||||
### 正确做法
|
||||
|
||||
```docker
|
||||
# ✅ 让 nginx 在前台运行
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CMD vs ENTRYPOINT
|
||||
|
||||
| 指令 | 用途 | 运行时行为 |
|
||||
|------|------|-----------|
|
||||
| **CMD** | 默认命令 | `docker run` 参数会**覆盖**它 |
|
||||
| **ENTRYPOINT** | 入口点 | `docker run` 参数会**追加**到它后面 |
|
||||
|
||||
### 单独使用 CMD
|
||||
|
||||
```docker
|
||||
# Dockerfile
|
||||
CMD ["curl", "-s", "http://example.com"]
|
||||
```
|
||||
|
||||
```bash
|
||||
$ docker run myimage # 执行默认命令
|
||||
$ docker run myimage curl -v ... # 完全覆盖
|
||||
```
|
||||
|
||||
### 搭配 ENTRYPOINT
|
||||
|
||||
```docker
|
||||
# Dockerfile
|
||||
ENTRYPOINT ["curl", "-s"]
|
||||
CMD ["http://example.com"]
|
||||
```
|
||||
|
||||
```bash
|
||||
$ docker run myimage # curl -s http://example.com
|
||||
$ docker run myimage http://other.com # curl -s http://other.com(参数覆盖)
|
||||
```
|
||||
|
||||
详见 [ENTRYPOINT 入口点](entrypoint.md) 章节。
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 优先使用 exec 格式
|
||||
|
||||
```docker
|
||||
# ✅ 推荐
|
||||
CMD ["python", "app.py"]
|
||||
|
||||
# ⚠️ 仅在需要 shell 特性时使用
|
||||
CMD ["sh", "-c", "echo $PATH && python app.py"]
|
||||
```
|
||||
|
||||
### 2. 确保应用在前台运行
|
||||
|
||||
```docker
|
||||
# ✅ 前台运行
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
CMD ["apache2ctl", "-D", "FOREGROUND"]
|
||||
CMD ["java", "-jar", "app.jar"]
|
||||
|
||||
# ❌ 不要使用后台服务命令
|
||||
CMD service nginx start
|
||||
CMD systemctl start nginx
|
||||
```
|
||||
|
||||
### 3. 使用双引号
|
||||
|
||||
```docker
|
||||
# ✅ 正确:双引号
|
||||
CMD ["node", "server.js"]
|
||||
|
||||
# ❌ 错误:单引号(JSON 不支持)
|
||||
CMD ['node', 'server.js']
|
||||
```
|
||||
|
||||
### 4. 配合 ENTRYPOINT 使用
|
||||
|
||||
```docker
|
||||
# 用于可配置参数的场景
|
||||
ENTRYPOINT ["python", "app.py"]
|
||||
CMD ["--port", "8080"]
|
||||
|
||||
# 运行时可以覆盖端口
|
||||
$ docker run myapp --port 9000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: CMD 可以写多个吗?
|
||||
|
||||
不可以。多个 CMD 只有最后一个生效:
|
||||
|
||||
```docker
|
||||
CMD ["echo", "first"]
|
||||
CMD ["echo", "second"] # 只有这个生效
|
||||
```
|
||||
|
||||
### Q: 如何在 CMD 中使用环境变量?
|
||||
|
||||
```docker
|
||||
# 方法1:使用 shell 格式
|
||||
CMD echo "Port is $PORT"
|
||||
|
||||
# 方法2:显式使用 sh -c
|
||||
CMD ["sh", "-c", "echo Port is $PORT"]
|
||||
```
|
||||
|
||||
### Q: 为什么我的容器不响应 Ctrl+C?
|
||||
|
||||
可能是使用了 shell 格式,信号被 sh 吃掉了:
|
||||
|
||||
```docker
|
||||
# ❌ 信号无法传递
|
||||
CMD python app.py
|
||||
|
||||
# ✅ 信号正确传递
|
||||
CMD ["python", "app.py"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 本章小结
|
||||
|
||||
| 要点 | 说明 |
|
||||
|------|------|
|
||||
| **作用** | 指定容器启动时的默认命令 |
|
||||
| **推荐格式** | exec 格式 `CMD ["程序", "参数"]` |
|
||||
| **覆盖方式** | `docker run image 新命令` |
|
||||
| **与 ENTRYPOINT** | CMD 作为 ENTRYPOINT 的默认参数 |
|
||||
| **核心原则** | 应用必须在前台运行 |
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- [ENTRYPOINT 入口点](entrypoint.md):固定的启动命令
|
||||
- [后台运行](../../05_container/daemon.md):容器前台/后台概念
|
||||
- [最佳实践](../../15_appendix/best_practices.md):Dockerfile 编写指南
|
||||
261
04_image/dockerfile/copy.md
Normal file
261
04_image/dockerfile/copy.md
Normal file
@@ -0,0 +1,261 @@
|
||||
# COPY 复制文件
|
||||
|
||||
## 基本语法
|
||||
|
||||
```docker
|
||||
COPY [选项] <源路径>... <目标路径>
|
||||
COPY [选项] ["<源路径1>", "<源路径2>", ... "<目标路径>"]
|
||||
```
|
||||
|
||||
`COPY` 指令将构建上下文中的文件或目录复制到镜像内。
|
||||
|
||||
---
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 复制单个文件
|
||||
|
||||
```docker
|
||||
# 复制文件到指定目录
|
||||
COPY package.json /app/
|
||||
|
||||
# 复制文件并重命名
|
||||
COPY config.json /app/settings.json
|
||||
```
|
||||
|
||||
### 复制多个文件
|
||||
|
||||
```docker
|
||||
# 复制多个指定文件
|
||||
COPY package.json package-lock.json /app/
|
||||
|
||||
# 使用通配符
|
||||
COPY *.json /app/
|
||||
COPY src/*.js /app/src/
|
||||
```
|
||||
|
||||
### 复制目录
|
||||
|
||||
```docker
|
||||
# 复制整个目录的内容(不是目录本身)
|
||||
COPY src/ /app/src/
|
||||
```
|
||||
|
||||
> ⚠️ **注意**:复制目录时,复制的是目录的**内容**,不包含目录本身。
|
||||
|
||||
```
|
||||
构建上下文: 镜像内:
|
||||
src/ /app/src/
|
||||
├── index.js → ├── index.js
|
||||
└── utils.js └── utils.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 通配符规则
|
||||
|
||||
COPY 支持 Go 的 `filepath.Match` 通配符规则:
|
||||
|
||||
| 通配符 | 说明 | 示例 |
|
||||
|--------|------|------|
|
||||
| `*` | 匹配任意字符序列 | `*.json` |
|
||||
| `?` | 匹配单个字符 | `config?.json` |
|
||||
| `[abc]` | 匹配括号内任一字符 | `[abc].txt` |
|
||||
| `[a-z]` | 匹配范围内字符 | `file[0-9].txt` |
|
||||
|
||||
```docker
|
||||
COPY hom* /mydir/ # home.txt, homework.md 等
|
||||
COPY hom?.txt /mydir/ # home.txt, homy.txt 等
|
||||
COPY app[0-9].js /app/ # app0.js ~ app9.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 目标路径
|
||||
|
||||
### 绝对路径
|
||||
|
||||
```docker
|
||||
COPY app.js /usr/src/app/
|
||||
```
|
||||
|
||||
### 相对路径(基于 WORKDIR)
|
||||
|
||||
```docker
|
||||
WORKDIR /app
|
||||
COPY package.json ./ # 复制到 /app/package.json
|
||||
COPY src/ ./src/ # 复制到 /app/src/
|
||||
```
|
||||
|
||||
### 自动创建目录
|
||||
|
||||
如果目标目录不存在,Docker 会自动创建:
|
||||
|
||||
```docker
|
||||
# /app/config/ 不存在也会自动创建
|
||||
COPY settings.json /app/config/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 修改文件所有者
|
||||
|
||||
使用 `--chown` 选项设置文件的用户和组:
|
||||
|
||||
```docker
|
||||
# 使用用户名和组名
|
||||
COPY --chown=node:node package.json /app/
|
||||
|
||||
# 使用 UID 和 GID
|
||||
COPY --chown=1000:1000 . /app/
|
||||
|
||||
# 只指定用户
|
||||
COPY --chown=node . /app/
|
||||
```
|
||||
|
||||
> 💡 结合 `USER` 指令使用,确保应用以非 root 用户运行。
|
||||
|
||||
---
|
||||
|
||||
## 保留文件元数据
|
||||
|
||||
COPY 会保留源文件的元数据:
|
||||
- 读、写、执行权限
|
||||
- 修改时间
|
||||
|
||||
这对于脚本文件特别重要:
|
||||
|
||||
```docker
|
||||
# start.sh 的可执行权限会被保留
|
||||
COPY start.sh /app/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## COPY vs ADD
|
||||
|
||||
| 特性 | COPY | ADD |
|
||||
|------|------|-----|
|
||||
| 复制本地文件 | ✅ | ✅ |
|
||||
| 自动解压 tar | ❌ | ✅ |
|
||||
| 支持 URL | ❌ | ✅(不推荐) |
|
||||
| 推荐程度 | ✅ **推荐** | ⚠️ 特殊场景使用 |
|
||||
|
||||
```docker
|
||||
# 推荐:使用 COPY
|
||||
COPY app.tar.gz /app/
|
||||
RUN tar -xzf /app/app.tar.gz
|
||||
|
||||
# ADD 会自动解压(行为不明显,不推荐)
|
||||
ADD app.tar.gz /app/
|
||||
```
|
||||
|
||||
> 笔者建议:除非需要自动解压 tar 文件,否则始终使用 COPY。明确的行为比隐式的魔法更好。
|
||||
|
||||
---
|
||||
|
||||
## 多阶段构建中的 COPY
|
||||
|
||||
### 从其他构建阶段复制
|
||||
|
||||
```docker
|
||||
# 构建阶段
|
||||
FROM node:20 AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# 生产阶段
|
||||
FROM nginx:alpine
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
```
|
||||
|
||||
### 使用 --link 优化缓存(BuildKit)
|
||||
|
||||
```docker
|
||||
# 使用 --link 后,文件以独立层添加,不依赖前序指令
|
||||
COPY --link --from=builder /app/dist /usr/share/nginx/html
|
||||
```
|
||||
|
||||
`--link` 的优势:
|
||||
- 更高效利用构建缓存
|
||||
- 并行化构建过程
|
||||
- 加速多阶段构建
|
||||
|
||||
---
|
||||
|
||||
## .dockerignore
|
||||
|
||||
使用 `.dockerignore` 排除不需要复制的文件:
|
||||
|
||||
```gitignore
|
||||
# .dockerignore
|
||||
node_modules
|
||||
.git
|
||||
.env
|
||||
*.log
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
```
|
||||
|
||||
这可以:
|
||||
- 减小构建上下文大小
|
||||
- 加速构建
|
||||
- 避免复制敏感文件
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 利用缓存,先复制依赖文件
|
||||
|
||||
```docker
|
||||
# ✅ 好:先复制依赖定义,再安装,最后复制代码
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
|
||||
# ❌ 差:一次性复制所有文件,代码变更会导致重新 npm install
|
||||
COPY . .
|
||||
RUN npm install
|
||||
```
|
||||
|
||||
### 2. 使用 .dockerignore
|
||||
|
||||
```docker
|
||||
# 确保 node_modules 不被复制
|
||||
COPY . .
|
||||
# .dockerignore 中应包含 node_modules
|
||||
```
|
||||
|
||||
### 3. 明确复制路径
|
||||
|
||||
```docker
|
||||
# ✅ 好:明确的路径
|
||||
COPY src/ /app/src/
|
||||
COPY package.json /app/
|
||||
|
||||
# ❌ 差:过于宽泛
|
||||
COPY . .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 本章小结
|
||||
|
||||
| 操作 | 示例 |
|
||||
|------|------|
|
||||
| 复制文件 | `COPY app.js /app/` |
|
||||
| 复制多个文件 | `COPY *.json /app/` |
|
||||
| 复制目录内容 | `COPY src/ /app/src/` |
|
||||
| 修改所有者 | `COPY --chown=node:node . /app/` |
|
||||
| 从构建阶段复制 | `COPY --from=builder /app/dist ./` |
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- [ADD 指令](add.md):复制和解压
|
||||
- [WORKDIR 指令](workdir.md):设置工作目录
|
||||
- [多阶段构建](../multistage-builds.md):优化镜像大小
|
||||
- [最佳实践](../../15_appendix/best_practices.md):Dockerfile 编写指南
|
||||
306
04_image/dockerfile/entrypoint.md
Normal file
306
04_image/dockerfile/entrypoint.md
Normal file
@@ -0,0 +1,306 @@
|
||||
# ENTRYPOINT 入口点
|
||||
|
||||
## 什么是 ENTRYPOINT
|
||||
|
||||
`ENTRYPOINT` 指定容器启动时运行的入口程序。与 CMD 不同,ENTRYPOINT 定义的命令不会被 `docker run` 的参数覆盖,而是**接收这些参数**。
|
||||
|
||||
> **核心作用**:让镜像像一个可执行程序一样使用,`docker run` 的参数作为这个程序的参数。
|
||||
|
||||
---
|
||||
|
||||
## 语法格式
|
||||
|
||||
| 格式 | 语法 | 推荐程度 |
|
||||
|------|------|---------|
|
||||
| **exec 格式** | `ENTRYPOINT ["可执行文件", "参数1"]` | ✅ **推荐** |
|
||||
| **shell 格式** | `ENTRYPOINT 命令 参数` | ⚠️ 不推荐 |
|
||||
|
||||
```docker
|
||||
# exec 格式(推荐)
|
||||
ENTRYPOINT ["nginx", "-g", "daemon off;"]
|
||||
|
||||
# shell 格式(不推荐)
|
||||
ENTRYPOINT nginx -g "daemon off;"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ENTRYPOINT vs CMD
|
||||
|
||||
### 核心区别
|
||||
|
||||
| 特性 | ENTRYPOINT | CMD |
|
||||
|------|------------|-----|
|
||||
| **定位** | 固定的入口程序 | 默认参数 |
|
||||
| **docker run 参数** | 追加为参数 | 完全覆盖 |
|
||||
| **覆盖方式** | `--entrypoint` | 直接指定命令 |
|
||||
| **适用场景** | 把镜像当命令用 | 提供默认行为 |
|
||||
|
||||
### 行为对比
|
||||
|
||||
```docker
|
||||
# 只用 CMD
|
||||
CMD ["curl", "-s", "http://example.com"]
|
||||
```
|
||||
|
||||
```bash
|
||||
$ docker run myimage # curl -s http://example.com
|
||||
$ docker run myimage -v # 执行 -v(错误!)
|
||||
$ docker run myimage curl -v ... # curl -v ...(完全替换)
|
||||
```
|
||||
|
||||
```docker
|
||||
# 只用 ENTRYPOINT
|
||||
ENTRYPOINT ["curl", "-s"]
|
||||
```
|
||||
|
||||
```bash
|
||||
$ docker run myimage # curl -s(缺参数)
|
||||
$ docker run myimage http://example.com # curl -s http://example.com ✓
|
||||
```
|
||||
|
||||
```docker
|
||||
# ENTRYPOINT + CMD 组合(推荐)
|
||||
ENTRYPOINT ["curl", "-s"]
|
||||
CMD ["http://example.com"]
|
||||
```
|
||||
|
||||
```bash
|
||||
$ docker run myimage # curl -s http://example.com(默认)
|
||||
$ docker run myimage http://other.com # curl -s http://other.com ✓
|
||||
$ docker run myimage -v http://other.com # curl -s -v http://other.com ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 场景一:让镜像像命令一样使用
|
||||
|
||||
### 需求
|
||||
|
||||
创建一个查询公网 IP 的"命令"镜像。
|
||||
|
||||
### 使用 CMD 的问题
|
||||
|
||||
```docker
|
||||
FROM ubuntu:24.04
|
||||
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||
CMD ["curl", "-s", "http://myip.ipip.net"]
|
||||
```
|
||||
|
||||
```bash
|
||||
$ docker run myip # ✓ 正常工作
|
||||
当前 IP:61.148.226.66
|
||||
|
||||
$ docker run myip -i # ✗ 错误!
|
||||
exec: "-i": executable file not found
|
||||
# -i 替换了整个 CMD,被当作可执行文件
|
||||
```
|
||||
|
||||
### 使用 ENTRYPOINT 解决
|
||||
|
||||
```docker
|
||||
FROM ubuntu:24.04
|
||||
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||
ENTRYPOINT ["curl", "-s", "http://myip.ipip.net"]
|
||||
```
|
||||
|
||||
```bash
|
||||
$ docker run myip # ✓ 正常工作
|
||||
当前 IP:61.148.226.66
|
||||
|
||||
$ docker run myip -i # ✓ 添加 -i 参数
|
||||
HTTP/1.1 200 OK
|
||||
...
|
||||
当前 IP:61.148.226.66
|
||||
```
|
||||
|
||||
### 交互图示
|
||||
|
||||
```
|
||||
ENTRYPOINT ["curl", "-s", "http://myip.ipip.net"]
|
||||
│
|
||||
docker run myip -i
|
||||
│
|
||||
▼
|
||||
curl -s http://myip.ipip.net -i
|
||||
└─────────────────────────────┘
|
||||
ENTRYPOINT + docker run 参数
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 场景二:启动前的准备工作
|
||||
|
||||
### 需求
|
||||
|
||||
在启动主服务前执行初始化脚本(如数据库迁移、权限设置)。
|
||||
|
||||
### 实现方式
|
||||
|
||||
```docker
|
||||
FROM redis:7-alpine
|
||||
COPY docker-entrypoint.sh /usr/local/bin/
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||
CMD ["redis-server"]
|
||||
```
|
||||
|
||||
**docker-entrypoint.sh**:
|
||||
|
||||
```bash
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# 准备工作
|
||||
echo "Initializing..."
|
||||
|
||||
# 如果第一个参数是 redis-server,以 redis 用户运行
|
||||
if [ "$1" = 'redis-server' ]; then
|
||||
chown -R redis:redis /data
|
||||
exec gosu redis "$@"
|
||||
fi
|
||||
|
||||
# 其他命令直接执行
|
||||
exec "$@"
|
||||
```
|
||||
|
||||
### 工作流程
|
||||
|
||||
```
|
||||
docker run redis docker run redis bash
|
||||
│ │
|
||||
▼ ▼
|
||||
docker-entrypoint.sh redis-server docker-entrypoint.sh bash
|
||||
│ │
|
||||
├─ 初始化 ├─ 初始化
|
||||
├─ chown -R redis:redis /data │
|
||||
└─ exec gosu redis redis-server └─ exec bash
|
||||
(以 redis 用户运行) (以 root 用户运行)
|
||||
```
|
||||
|
||||
### 关键点
|
||||
|
||||
1. **exec "$@"**:用传入的参数替换当前进程,确保信号正确传递
|
||||
2. **条件判断**:根据 CMD 不同执行不同逻辑
|
||||
3. **用户切换**:使用 `gosu` 切换用户(比 `su` 更适合容器)
|
||||
|
||||
---
|
||||
|
||||
## 场景三:带参数的应用
|
||||
|
||||
```docker
|
||||
FROM python:3.12-slim
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
ENTRYPOINT ["python", "app.py"]
|
||||
CMD ["--host", "0.0.0.0", "--port", "8080"]
|
||||
```
|
||||
|
||||
```bash
|
||||
# 使用默认参数
|
||||
$ docker run myapp
|
||||
# 执行: python app.py --host 0.0.0.0 --port 8080
|
||||
|
||||
# 覆盖参数
|
||||
$ docker run myapp --host 0.0.0.0 --port 9000
|
||||
# 执行: python app.py --host 0.0.0.0 --port 9000
|
||||
|
||||
# 完全不同的参数
|
||||
$ docker run myapp --help
|
||||
# 执行: python app.py --help
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 覆盖 ENTRYPOINT
|
||||
|
||||
使用 `--entrypoint` 参数覆盖:
|
||||
|
||||
```bash
|
||||
# 正常运行
|
||||
$ docker run myimage
|
||||
|
||||
# 覆盖 ENTRYPOINT 进入 shell 调试
|
||||
$ docker run --entrypoint /bin/sh myimage
|
||||
|
||||
# 覆盖 ENTRYPOINT 并传入参数
|
||||
$ docker run --entrypoint /bin/cat myimage /etc/os-release
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ENTRYPOINT 与 CMD 组合表
|
||||
|
||||
| ENTRYPOINT | CMD | 最终执行命令 |
|
||||
|------------|-----|-------------|
|
||||
| 无 | 无 | 无(容器无法启动) |
|
||||
| 无 | `["cmd", "p1"]` | `cmd p1` |
|
||||
| `["ep", "p1"]` | 无 | `ep p1` |
|
||||
| `["ep", "p1"]` | `["cmd", "p2"]` | `ep p1 cmd p2` |
|
||||
| `ep p1`(shell) | `["cmd", "p2"]` | `/bin/sh -c "ep p1"`(CMD 被忽略) |
|
||||
|
||||
> ⚠️ **注意**:shell 格式的 ENTRYPOINT 会忽略 CMD!
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 使用 exec 格式
|
||||
|
||||
```docker
|
||||
# ✅ 推荐
|
||||
ENTRYPOINT ["python", "app.py"]
|
||||
|
||||
# ❌ 避免 shell 格式
|
||||
ENTRYPOINT python app.py
|
||||
```
|
||||
|
||||
### 2. 提供有意义的默认参数
|
||||
|
||||
```docker
|
||||
ENTRYPOINT ["nginx"]
|
||||
CMD ["-g", "daemon off;"]
|
||||
```
|
||||
|
||||
### 3. 入口脚本使用 exec
|
||||
|
||||
```bash
|
||||
#!/bin/sh
|
||||
# 准备工作...
|
||||
|
||||
# 使用 exec 替换当前进程
|
||||
exec "$@"
|
||||
```
|
||||
|
||||
### 4. 处理信号
|
||||
|
||||
确保 ENTRYPOINT 脚本能正确传递信号:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
trap 'kill -TERM $PID' TERM INT
|
||||
|
||||
# 启动应用
|
||||
app "$@" &
|
||||
PID=$!
|
||||
|
||||
# 等待应用退出
|
||||
wait $PID
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 本章小结
|
||||
|
||||
| ENTRYPOINT | CMD | 适用场景 |
|
||||
|------------|-----|---------|
|
||||
| ✓ | ✗ | 镜像作为固定命令使用 |
|
||||
| ✗ | ✓ | 简单的默认命令 |
|
||||
| ✓ | ✓ | **推荐**:固定命令 + 可配置参数 |
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- [CMD 容器启动命令](cmd.md):默认命令
|
||||
- [最佳实践](../../15_appendix/best_practices.md):启动命令设计
|
||||
- [后台运行](../../05_container/daemon.md):前台/后台概念
|
||||
248
04_image/dockerfile/env.md
Normal file
248
04_image/dockerfile/env.md
Normal file
@@ -0,0 +1,248 @@
|
||||
# ENV 设置环境变量
|
||||
|
||||
## 基本语法
|
||||
|
||||
```docker
|
||||
# 格式一:单个变量
|
||||
ENV <key> <value>
|
||||
|
||||
# 格式二:多个变量(推荐)
|
||||
ENV <key1>=<value1> <key2>=<value2> ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 设置单个变量
|
||||
|
||||
```docker
|
||||
ENV NODE_VERSION 20.10.0
|
||||
ENV APP_ENV production
|
||||
```
|
||||
|
||||
### 设置多个变量
|
||||
|
||||
```docker
|
||||
ENV NODE_VERSION=20.10.0 \
|
||||
APP_ENV=production \
|
||||
APP_NAME="My Application"
|
||||
```
|
||||
|
||||
> 💡 包含空格的值用双引号括起来。
|
||||
|
||||
---
|
||||
|
||||
## 环境变量的作用
|
||||
|
||||
### 1. 后续指令中使用
|
||||
|
||||
```docker
|
||||
ENV NODE_VERSION=20.10.0
|
||||
|
||||
# 在 RUN 中使用
|
||||
RUN curl -fsSL https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz \
|
||||
| tar -xJ -C /usr/local --strip-components=1
|
||||
|
||||
# 在 WORKDIR 中使用
|
||||
ENV APP_HOME=/app
|
||||
WORKDIR $APP_HOME
|
||||
|
||||
# 在 COPY 中使用
|
||||
COPY . $APP_HOME
|
||||
```
|
||||
|
||||
### 2. 容器运行时使用
|
||||
|
||||
```docker
|
||||
ENV DATABASE_URL=postgres://localhost/mydb
|
||||
```
|
||||
|
||||
应用代码中可以读取:
|
||||
|
||||
```python
|
||||
import os
|
||||
db_url = os.environ.get('DATABASE_URL')
|
||||
```
|
||||
|
||||
```javascript
|
||||
const dbUrl = process.env.DATABASE_URL;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 支持环境变量的指令
|
||||
|
||||
以下指令可以使用 `$变量名` 或 `${变量名}` 格式:
|
||||
|
||||
| 指令 | 示例 |
|
||||
|------|------|
|
||||
| `RUN` | `RUN echo $VERSION` |
|
||||
| `CMD` | `CMD ["sh", "-c", "echo $HOME"]` |
|
||||
| `ENTRYPOINT` | 同上 |
|
||||
| `COPY` | `COPY . $APP_HOME` |
|
||||
| `ADD` | `ADD app.tar.gz $APP_HOME` |
|
||||
| `WORKDIR` | `WORKDIR $APP_HOME` |
|
||||
| `EXPOSE` | `EXPOSE $PORT` |
|
||||
| `VOLUME` | `VOLUME $DATA_DIR` |
|
||||
| `USER` | `USER $USERNAME` |
|
||||
| `LABEL` | `LABEL version=$VERSION` |
|
||||
| `FROM` | `FROM node:$NODE_VERSION` |
|
||||
|
||||
---
|
||||
|
||||
## 运行时覆盖
|
||||
|
||||
使用 `-e` 或 `--env` 覆盖 Dockerfile 中定义的环境变量:
|
||||
|
||||
```bash
|
||||
# 覆盖单个变量
|
||||
$ docker run -e APP_ENV=development myimage
|
||||
|
||||
# 覆盖多个变量
|
||||
$ docker run -e APP_ENV=development -e DEBUG=true myimage
|
||||
|
||||
# 从环境变量文件读取
|
||||
$ docker run --env-file .env myimage
|
||||
```
|
||||
|
||||
### .env 文件格式
|
||||
|
||||
```bash
|
||||
# .env
|
||||
APP_ENV=development
|
||||
DEBUG=true
|
||||
DATABASE_URL=postgres://localhost/mydb
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ENV vs ARG
|
||||
|
||||
| 特性 | ENV | ARG |
|
||||
|------|-----|-----|
|
||||
| **生效时间** | 构建时 + 运行时 | 仅构建时 |
|
||||
| **持久性** | 写入镜像,运行时可用 | 构建后消失 |
|
||||
| **覆盖方式** | `docker run -e` | `docker build --build-arg` |
|
||||
| **适用场景** | 应用配置 | 构建参数(如版本号) |
|
||||
|
||||
### 组合使用
|
||||
|
||||
```docker
|
||||
# ARG 接收构建时参数
|
||||
ARG NODE_VERSION=20
|
||||
|
||||
# ENV 保存到运行时
|
||||
ENV NODE_VERSION=$NODE_VERSION
|
||||
|
||||
# 后续指令使用
|
||||
RUN curl -fsSL https://nodejs.org/dist/v${NODE_VERSION}/...
|
||||
```
|
||||
|
||||
```bash
|
||||
# 构建时指定版本
|
||||
$ docker build --build-arg NODE_VERSION=18 -t myapp .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 统一管理版本号
|
||||
|
||||
```docker
|
||||
# ✅ 好:版本集中管理
|
||||
ENV NGINX_VERSION=1.25.0 \
|
||||
NODE_VERSION=20.10.0 \
|
||||
PYTHON_VERSION=3.12.0
|
||||
|
||||
RUN apt-get install nginx=${NGINX_VERSION}
|
||||
|
||||
# ❌ 差:版本分散在各处
|
||||
RUN apt-get install nginx=1.25.0
|
||||
```
|
||||
|
||||
### 2. 不要存储敏感信息
|
||||
|
||||
```docker
|
||||
# ❌ 错误:密码写入镜像
|
||||
ENV DB_PASSWORD=secret123
|
||||
|
||||
# ✅ 正确:运行时传入
|
||||
# docker run -e DB_PASSWORD=xxx myimage
|
||||
```
|
||||
|
||||
### 3. 为应用提供合理默认值
|
||||
|
||||
```docker
|
||||
ENV APP_ENV=production \
|
||||
APP_PORT=8080 \
|
||||
LOG_LEVEL=info
|
||||
```
|
||||
|
||||
### 4. 使用有意义的变量名
|
||||
|
||||
```docker
|
||||
# ✅ 好:清晰的命名
|
||||
ENV REDIS_HOST=localhost \
|
||||
REDIS_PORT=6379
|
||||
|
||||
# ❌ 差:模糊的命名
|
||||
ENV HOST=localhost \
|
||||
PORT=6379
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 环境变量在 CMD 中不展开
|
||||
|
||||
exec 格式不会自动展开环境变量:
|
||||
|
||||
```docker
|
||||
# ❌ 不会展开 $PORT
|
||||
CMD ["python", "app.py", "--port", "$PORT"]
|
||||
|
||||
# ✅ 使用 shell 格式或显式调用 sh
|
||||
CMD ["sh", "-c", "python app.py --port $PORT"]
|
||||
```
|
||||
|
||||
### Q: 如何查看容器的环境变量
|
||||
|
||||
```bash
|
||||
$ docker inspect mycontainer --format '{{json .Config.Env}}'
|
||||
$ docker exec mycontainer env
|
||||
```
|
||||
|
||||
### Q: 多行 ENV 还是多个 ENV
|
||||
|
||||
```docker
|
||||
# ✅ 推荐:减少层数
|
||||
ENV VAR1=value1 \
|
||||
VAR2=value2 \
|
||||
VAR3=value3
|
||||
|
||||
# ⚠️ 多个 ENV 会创建多层
|
||||
ENV VAR1=value1
|
||||
ENV VAR2=value2
|
||||
ENV VAR3=value3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 本章小结
|
||||
|
||||
| 要点 | 说明 |
|
||||
|------|------|
|
||||
| **语法** | `ENV KEY=value` |
|
||||
| **作用范围** | 构建时 + 运行时 |
|
||||
| **覆盖方式** | `docker run -e KEY=value` |
|
||||
| **与 ARG** | ARG 仅构建时,ENV 持久化到运行时 |
|
||||
| **安全** | 不要存储敏感信息 |
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- [ARG 构建参数](arg.md):构建时变量
|
||||
- [Compose 环境变量](../../compose/compose_file.md):Compose 中的环境变量
|
||||
- [最佳实践](../../15_appendix/best_practices.md):Dockerfile 编写指南
|
||||
219
04_image/dockerfile/expose.md
Normal file
219
04_image/dockerfile/expose.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# EXPOSE 声明端口
|
||||
|
||||
## 基本语法
|
||||
|
||||
```docker
|
||||
EXPOSE <端口> [<端口>/<协议>...]
|
||||
```
|
||||
|
||||
`EXPOSE` 声明容器运行时提供服务的端口。这是一个**文档性质的声明**,告诉使用者容器会监听哪些端口。
|
||||
|
||||
---
|
||||
|
||||
## 基本用法
|
||||
|
||||
```docker
|
||||
# 声明单个端口
|
||||
EXPOSE 80
|
||||
|
||||
# 声明多个端口
|
||||
EXPOSE 80 443
|
||||
|
||||
# 声明 TCP 和 UDP 端口
|
||||
EXPOSE 80/tcp
|
||||
EXPOSE 53/udp
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## EXPOSE 的作用
|
||||
|
||||
### 1. 文档说明
|
||||
|
||||
告诉镜像使用者,容器将在哪些端口提供服务:
|
||||
|
||||
```docker
|
||||
# 使用者一看就知道这是 web 应用
|
||||
EXPOSE 80 443
|
||||
```
|
||||
|
||||
```bash
|
||||
# 查看镜像暴露的端口
|
||||
$ docker inspect nginx --format '{{.Config.ExposedPorts}}'
|
||||
map[80/tcp:{}]
|
||||
```
|
||||
|
||||
### 2. 配合 -P 使用
|
||||
|
||||
使用 `docker run -P` 时,Docker 会自动映射 EXPOSE 的端口到宿主机随机端口:
|
||||
|
||||
```docker
|
||||
# Dockerfile
|
||||
EXPOSE 80
|
||||
```
|
||||
|
||||
```bash
|
||||
$ docker run -P nginx
|
||||
$ docker port $(docker ps -q)
|
||||
80/tcp -> 0.0.0.0:32768
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## EXPOSE vs -p
|
||||
|
||||
| 特性 | EXPOSE | -p |
|
||||
|------|--------|-----|
|
||||
| **位置** | Dockerfile | docker run 命令 |
|
||||
| **作用** | 声明/文档 | 实际端口映射 |
|
||||
| **是否必需** | 否 | 是(外部访问时) |
|
||||
| **映射发生时** | 不发生 | 运行时发生 |
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ EXPOSE 80 │
|
||||
│ ↓ │
|
||||
│ 仅声明意图 │
|
||||
│ └───────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ docker run -p │
|
||||
│ ↓ │
|
||||
│ ┌─────────────────────┐ │
|
||||
│ │ 实际端口映射 │ │
|
||||
│ │ 宿主机 ←→ 容器 │ │
|
||||
│ └─────────────────────┘ │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 没有 EXPOSE 也能 -p
|
||||
|
||||
```docker
|
||||
# 即使没有 EXPOSE,也可以使用 -p
|
||||
FROM nginx
|
||||
# 没有 EXPOSE
|
||||
```
|
||||
|
||||
```bash
|
||||
# 仍然可以映射端口
|
||||
$ docker run -p 8080:80 mynginx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见误解
|
||||
|
||||
### 误解:EXPOSE 会打开端口
|
||||
|
||||
```docker
|
||||
# ❌ 错误理解:这不会让容器可从外部访问
|
||||
EXPOSE 80
|
||||
```
|
||||
|
||||
EXPOSE 不会:
|
||||
- 自动进行端口映射
|
||||
- 让服务可从外部访问
|
||||
- 在容器启动时开启端口监听
|
||||
|
||||
EXPOSE 只是元数据声明。容器是否实际监听该端口,取决于容器内的应用。
|
||||
|
||||
### 正确理解
|
||||
|
||||
```docker
|
||||
# Dockerfile
|
||||
FROM nginx
|
||||
EXPOSE 80 # 1. 声明:这个容器会在 80 端口提供服务
|
||||
```
|
||||
|
||||
```bash
|
||||
# 运行:需要 -p 才能从外部访问
|
||||
$ docker run -p 8080:80 nginx # 2. 映射:宿主机 8080 → 容器 80
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 总是声明应用使用的端口
|
||||
|
||||
```docker
|
||||
# Web 服务
|
||||
FROM nginx
|
||||
EXPOSE 80 443
|
||||
|
||||
# 数据库
|
||||
FROM postgres
|
||||
EXPOSE 5432
|
||||
|
||||
# Redis
|
||||
FROM redis
|
||||
EXPOSE 6379
|
||||
```
|
||||
|
||||
### 2. 使用明确的协议
|
||||
|
||||
```docker
|
||||
# 默认是 TCP
|
||||
EXPOSE 80
|
||||
|
||||
# 明确指定 UDP
|
||||
EXPOSE 53/udp
|
||||
|
||||
# 同时支持 TCP 和 UDP
|
||||
EXPOSE 53/tcp 53/udp
|
||||
```
|
||||
|
||||
### 3. 与应用实际端口保持一致
|
||||
|
||||
```docker
|
||||
# ✅ 好:EXPOSE 与应用端口一致
|
||||
ENV PORT=3000
|
||||
EXPOSE 3000
|
||||
CMD ["node", "server.js"]
|
||||
|
||||
# ❌ 差:EXPOSE 与应用端口不一致(误导)
|
||||
EXPOSE 80
|
||||
CMD ["node", "server.js"] # 实际监听 3000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用环境变量
|
||||
|
||||
```docker
|
||||
ARG PORT=80
|
||||
EXPOSE $PORT
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 在 Compose 中
|
||||
|
||||
```yaml
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:80" # 映射端口(类似 -p)
|
||||
expose:
|
||||
- "80" # 仅声明(类似 EXPOSE)
|
||||
```
|
||||
|
||||
`expose` 在 Compose 中仅用于容器间通信的文档说明,不进行端口映射。
|
||||
|
||||
---
|
||||
|
||||
## 本章小结
|
||||
|
||||
| 要点 | 说明 |
|
||||
|------|------|
|
||||
| **作用** | 声明容器提供服务的端口(文档) |
|
||||
| **不会** | 自动映射端口或开放外部访问 |
|
||||
| **配合** | `docker run -P` 自动映射 |
|
||||
| **外部访问** | 需要 `-p 宿主机端口:容器端口` |
|
||||
| **语法** | `EXPOSE 80` 或 `EXPOSE 80/tcp` |
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- [网络配置](../../network/README.md):Docker 网络详解
|
||||
- [端口映射](../../network/port_bindingbindingbinding.md):-p 参数详解
|
||||
- [Compose 端口](../../compose/compose_file.md):Compose 中的端口配置
|
||||
206
04_image/dockerfile/healthcheck.md
Normal file
206
04_image/dockerfile/healthcheck.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# HEALTHCHECK 健康检查
|
||||
|
||||
## 基本语法
|
||||
|
||||
```docker
|
||||
HEALTHCHECK [选项] CMD <命令>
|
||||
HEALTHCHECK NONE
|
||||
```
|
||||
|
||||
`HEALTHCHECK` 指令告诉 Docker 如何判断容器状态是否正常。这是保障服务高可用的重要机制。
|
||||
|
||||
---
|
||||
|
||||
## 为什么需要 HEALTHCHECK
|
||||
|
||||
在没有 HEALTHCHECK 之前,Docker 只能通过**进程退出码**来判断容器状态。
|
||||
|
||||
**问题场景**:
|
||||
- Web 服务死锁,无法响应请求,但进程仍在运行
|
||||
- 数据库正在启动中,尚未准备好接受连接
|
||||
- 应用陷入死循环,CPU 爆满但进程存活
|
||||
|
||||
**引入 HEALTHCHECK 后**:
|
||||
Docker 定期执行指定的检查命令,根据返回值判断容器是否"健康"。
|
||||
|
||||
```
|
||||
容器状态转换:
|
||||
Starting ──成功──> Healthy ──失败N次──> Unhealthy
|
||||
▲ │
|
||||
└──────成功──────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 基本用法
|
||||
|
||||
### Web 服务检查
|
||||
|
||||
```docker
|
||||
FROM nginx
|
||||
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
|
||||
CMD curl -fs http://localhost/ || exit 1
|
||||
```
|
||||
|
||||
### 命令返回值
|
||||
|
||||
- `0`: 成功 (healthy)
|
||||
- `1`: 失败 (unhealthy)
|
||||
- `2`: 保留值 (不使用)
|
||||
|
||||
### 常用选项
|
||||
|
||||
| 选项 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `--interval` | 两次检查的间隔 | 30s |
|
||||
| `--timeout` | 检查命令的超时时间 | 30s |
|
||||
| `--start-period` | 启动缓冲期(期间失败不计入次数) | 0s |
|
||||
| `--retries` | 连续失败多少次标记为 unhealthy | 3 |
|
||||
|
||||
---
|
||||
|
||||
## 屏蔽健康检查
|
||||
|
||||
如果基础镜像定义了 HEALTHCHECK,但你不想使用它:
|
||||
|
||||
```docker
|
||||
FROM my-base-image
|
||||
HEALTHCHECK NONE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见检查脚本
|
||||
|
||||
### HTTP 服务
|
||||
|
||||
使用 `curl` 或 `wget`:
|
||||
|
||||
```docker
|
||||
# 使用 curl
|
||||
HEALTHCHECK CMD curl -f http://localhost/ || exit 1
|
||||
|
||||
# 使用 wget (Alpine 默认包含)
|
||||
HEALTHCHECK CMD wget -q --spider http://localhost/ || exit 1
|
||||
```
|
||||
|
||||
### 数据库
|
||||
|
||||
```docker
|
||||
# MySQL
|
||||
HEALTHCHECK CMD mysqladmin ping -h localhost || exit 1
|
||||
|
||||
# Redis
|
||||
HEALTHCHECK CMD redis-cli ping || exit 1
|
||||
```
|
||||
|
||||
### 自定义脚本
|
||||
|
||||
```docker
|
||||
COPY healthcheck.sh /usr/local/bin/
|
||||
HEALTHCHECK CMD ["healthcheck.sh"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 在 Compose 中使用
|
||||
|
||||
可以在 `docker-compose.yml` 中覆盖或定义健康检查:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
web:
|
||||
image: nginx
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost"]
|
||||
interval: 1m30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
```
|
||||
|
||||
带健康检查的依赖启动:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
web:
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy # 等待 db 变健康才启动 web
|
||||
db:
|
||||
image: mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 查看健康状态
|
||||
|
||||
```bash
|
||||
# 查看容器状态(包含健康信息)
|
||||
$ docker ps
|
||||
CONTAINER ID STATUS
|
||||
abc123 Up 1 minute (healthy)
|
||||
def456 Up 2 minutes (unhealthy)
|
||||
|
||||
# 查看详细健康日志
|
||||
$ docker inspect --format '{{json .State.Health}}' mycontainer | jq
|
||||
{
|
||||
"Status": "healthy",
|
||||
"FailingStreak": 0,
|
||||
"Log": [
|
||||
{
|
||||
"Start": "...",
|
||||
"End": "...",
|
||||
"ExitCode": 0,
|
||||
"Output": "..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 避免副作用
|
||||
|
||||
健康检查会被频繁执行,不要在检查脚本中进行写操作或消耗大量资源的操作。
|
||||
|
||||
### 2. 使用轻量级工具
|
||||
|
||||
优先使用镜像中已有的工具(如 `wget`),避免为了健康检查安装庞大的依赖(如 `curl`)。
|
||||
|
||||
### 3. 设置合理的 Start Period
|
||||
|
||||
应用启动可能需要时间(如 Java 应用)。设置 `--start-period` 可以防止在启动阶段因检查失败而误判。
|
||||
|
||||
```docker
|
||||
# 给应用 1 分钟启动时间
|
||||
HEALTHCHECK --start-period=60s CMD curl -f http://localhost/ || exit 1
|
||||
```
|
||||
|
||||
### 4. 只检查核心依赖
|
||||
|
||||
健康检查应主要关注**当前服务**是否可用,而不是检查其下游依赖(数据库等)。下游依赖的检查应由应用逻辑处理。
|
||||
|
||||
---
|
||||
|
||||
## 本章小结
|
||||
|
||||
| 要点 | 说明 |
|
||||
|------|------|
|
||||
| **作用** | 检测容器应用是否真实可用 |
|
||||
| **命令** | `HEALTHCHECK [选项] CMD command` |
|
||||
| **状态** | starting, healthy, unhealthy |
|
||||
| **Compose** | 支持 `condition: service_healthy` 依赖 |
|
||||
| **注意** | 避免副作用,节省资源 |
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- [CMD 容器启动命令](cmd.md):启动主进程
|
||||
- [Compose 模板文件](../../compose/compose_file.md):Compose 中的健康检查
|
||||
- [Docker 调试](../../15_appendix/debug.md):容器排障
|
||||
154
04_image/dockerfile/label.md
Normal file
154
04_image/dockerfile/label.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# LABEL 为镜像添加元数据
|
||||
|
||||
## 基本语法
|
||||
|
||||
```docker
|
||||
LABEL <key>=<value> <key>=<value> ...
|
||||
```
|
||||
|
||||
`LABEL` 指令以键值对的形式给镜像添加元数据。这些数据不会影响镜像的功能,但可以帮助用户理解镜像,或被自动化工具使用。
|
||||
|
||||
---
|
||||
|
||||
## 为什么需要 LABEL
|
||||
|
||||
1. **版本管理**:记录版本号、构建时间、Git Commit ID
|
||||
2. **联系信息**:维护者邮箱、文档地址、支持渠道
|
||||
3. **自动化工具**: CI/CD 工具可以读取标签触发操作
|
||||
4. **许可证信息**:声明开源协议
|
||||
|
||||
---
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 定义单个标签
|
||||
|
||||
```docker
|
||||
LABEL version="1.0"
|
||||
LABEL description="这是一个 Web 应用服务器"
|
||||
```
|
||||
|
||||
### 定义多个标签(推荐)
|
||||
|
||||
```docker
|
||||
LABEL maintainer="user@example.com" \
|
||||
version="1.2.0" \
|
||||
description="My App Description" \
|
||||
org.opencontainers.image.authors="Yeasy"
|
||||
```
|
||||
|
||||
> 💡 包含空格的值需要用引号括起来。
|
||||
|
||||
---
|
||||
|
||||
## 常用标签规范 (OCI Annotations)
|
||||
|
||||
为了标准和互操作性,推荐使用 [OCI Image Format Specification](https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys) 定义的标准标签:
|
||||
|
||||
| 标签 Key | 说明 | 示例 |
|
||||
|----------|------|------|
|
||||
| `org.opencontainers.image.created` | 构建时间(RFC 3339) | `2024-01-01T00:00:00Z` |
|
||||
| `org.opencontainers.image.authors` | 作者/维护者 | `support@example.com` |
|
||||
| `org.opencontainers.image.url` | 项目主页 | `https://example.com` |
|
||||
| `org.opencontainers.image.documentation`| 文档地址 | `https://example.com/docs` |
|
||||
| `org.opencontainers.image.source` | 源码仓库 | `https://github.com/user/repo` |
|
||||
| `org.opencontainers.image.version` | 版本号 | `1.0.0` |
|
||||
| `org.opencontainers.image.licenses` | 许可证 | `MIT` |
|
||||
| `org.opencontainers.image.title` | 镜像标题 | `My App` |
|
||||
| `org.opencontainers.image.description` | 描述 | `Production ready web server` |
|
||||
|
||||
### 示例
|
||||
|
||||
```docker
|
||||
LABEL org.opencontainers.image.authors="yeasy" \
|
||||
org.opencontainers.image.documentation="https://yeasy.gitbooks.io" \
|
||||
org.opencontainers.image.source="https://github.com/yeasy/docker_practice" \
|
||||
org.opencontainers.image.licenses="MIT"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MAINTAINER 指令(已废弃)
|
||||
|
||||
旧版本的 Dockerfile 中常看到 `MAINTAINER` 指令:
|
||||
|
||||
```docker
|
||||
# ❌ 已弃用
|
||||
MAINTAINER user@example.com
|
||||
```
|
||||
|
||||
现在推荐使用 `LABEL`:
|
||||
|
||||
```docker
|
||||
# ✅ 推荐
|
||||
LABEL maintainer="user@example.com"
|
||||
# 或
|
||||
LABEL org.opencontainers.image.authors="user@example.com"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 动态标签
|
||||
|
||||
配合 `ARG` 使用,可以在构建时动态注入标签:
|
||||
|
||||
```docker
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
|
||||
LABEL org.opencontainers.image.created=$BUILD_DATE \
|
||||
org.opencontainers.image.revision=$VCS_REF
|
||||
```
|
||||
|
||||
构建命令:
|
||||
|
||||
```bash
|
||||
$ docker build \
|
||||
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
|
||||
--build-arg VCS_REF=$(git rev-parse --short HEAD) \
|
||||
.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 查看标签
|
||||
|
||||
### docker inspect
|
||||
|
||||
查看镜像的标签信息:
|
||||
|
||||
```bash
|
||||
$ docker inspect nginx --format '{{json .Config.Labels}}' | jq
|
||||
{
|
||||
"maintainer": "NGINX Docker Maintainers <docker-maint@nginx.com>"
|
||||
}
|
||||
```
|
||||
|
||||
### 过滤器
|
||||
|
||||
可以使用标签过滤镜像:
|
||||
|
||||
```bash
|
||||
# 列出作者是 yeasy 的所有镜像
|
||||
$ docker images --filter "label=org.opencontainers.image.authors=yeasy"
|
||||
|
||||
# 删除所有带有特定标签的镜像
|
||||
$ docker rmi $(docker images -q --filter "label=stage=builder")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 本章小结
|
||||
|
||||
| 要点 | 说明 |
|
||||
|------|------|
|
||||
| **作用** | 添加 key-value 元数据 |
|
||||
| **语法** | `LABEL k=v k=v ...` |
|
||||
| **规范** | 推荐使用 OCI 标准标签 |
|
||||
| **弃用** | 不要再使用 `MAINTAINER` |
|
||||
| **查看** | `docker inspect` |
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- [OCI 标签规范](https://github.com/opencontainers/image-spec/blob/main/annotations.md)
|
||||
- [Dockerfile 最佳实践](../../15_appendix/best_practices.md)
|
||||
151
04_image/dockerfile/onbuild.md
Normal file
151
04_image/dockerfile/onbuild.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# ONBUILD 为他人做嫁衣裳
|
||||
|
||||
## 基本语法
|
||||
|
||||
```docker
|
||||
ONBUILD <其它指令>
|
||||
```
|
||||
|
||||
`ONBUILD` 是一个特殊的指令,它后面跟的是其它指令(如 `RUN`, `COPY` 等),这些指令**在当前镜像构建时不会执行**,只有当以当前镜像为基础镜像去构建下一级镜像时才会被执行。
|
||||
|
||||
---
|
||||
|
||||
## 为什么需要 ONBUILD
|
||||
|
||||
`ONBUILD` 主要用于制作**语言栈基础镜像**或**框架基础镜像**。
|
||||
|
||||
### 场景:维护 Node.js 项目
|
||||
|
||||
假设你有多个 Node.js 项目,它们的构建流程都一样:
|
||||
1. 创建目录
|
||||
2. 复制 `package.json`
|
||||
3. 执行 `npm install`
|
||||
4. 复制源码
|
||||
5. 启动应用
|
||||
|
||||
如果不使用 `ONBUILD`,每个项目的 Dockerfile 都要重复这些步骤,且通过 `COPY` 复制文件时,基础镜像无法预知子项目的文件名。
|
||||
|
||||
### 使用 ONBUILD 的解决方案
|
||||
|
||||
**基础镜像 (my-node-base)**:
|
||||
|
||||
```docker
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
|
||||
# 这些指令将在子镜像构建时执行
|
||||
ONBUILD COPY package*.json ./
|
||||
ONBUILD RUN npm install
|
||||
ONBUILD COPY . .
|
||||
|
||||
CMD ["npm", "start"]
|
||||
```
|
||||
|
||||
**子项目 Dockerfile**:
|
||||
|
||||
```docker
|
||||
FROM my-node-base
|
||||
# 只需要一行!
|
||||
# 构建时会自动执行 COPY 和 RUN
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 执行机制
|
||||
|
||||
```
|
||||
基础镜像构建:
|
||||
Dockerfile (含 ONBUILD) ──build──> 基础镜像 (记录了 ONBUILD 触发器)
|
||||
(指令未执行)
|
||||
|
||||
子镜像构建:
|
||||
FROM 基础镜像 ──build──> 读取基础镜像触发器 ──> 执行触发器指令 ──> 继续执行子 Dockerfile
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见使用场景
|
||||
|
||||
### 1. 自动处理依赖安装
|
||||
|
||||
```docker
|
||||
# Python 基础镜像
|
||||
ONBUILD COPY requirements.txt ./
|
||||
ONBUILD RUN pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. 自动编译代码
|
||||
|
||||
```docker
|
||||
# Go 基础镜像
|
||||
ONBUILD COPY . .
|
||||
ONBUILD RUN go build -o app main.go
|
||||
```
|
||||
|
||||
### 3. 处理静态资源
|
||||
|
||||
```docker
|
||||
# Nginx 静态网站基础镜像
|
||||
ONBUILD COPY dist/ /usr/share/nginx/html/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 1. 继承性限制
|
||||
|
||||
`ONBUILD` 指令**只会继承一次**。
|
||||
- 镜像 A (含 ONBUILD)
|
||||
- 镜像 B (FROM A) -> 触发 ONBUILD
|
||||
- 镜像 C (FROM B) -> **不会**再次触发 ONBUILD
|
||||
|
||||
### 2. 构建上下文
|
||||
|
||||
子镜像构建时,`ONBUILD COPY . .` 中的 `.` 指的是**子项目**的构建上下文,而不是基础镜像的上下文。
|
||||
|
||||
### 3. 不允许级联
|
||||
|
||||
`ONBUILD ONBUILD` 是非法的。你不能写 `ONBUILD ONBUILD COPY ...`。
|
||||
|
||||
### 4. 可能会导致构建失败
|
||||
|
||||
由于 `ONBUILD` 实际上是在子镜像中执行指令,如果子项目的上下文不满足要求(例如缺少 `package.json`),会导致子镜像构建失败,且错误信息可能比较隐晦。
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 命名规范
|
||||
|
||||
建议在镜像标签中添加 `-onbuild` 后缀,明确告知使用者该镜像包含触发器。
|
||||
|
||||
```
|
||||
node:20-onbuild
|
||||
python:3.12-onbuild
|
||||
```
|
||||
|
||||
### 2. 避免执行耗时操作
|
||||
|
||||
尽量不要在 `ONBUILD` 中执行过于耗时或不确定的操作(如更新系统软件),这会让子镜像构建变得缓慢且不可控。
|
||||
|
||||
### 3. 清理工作
|
||||
|
||||
如果 `ONBUILD` 指令产生了临时文件,最好在同一个指令链中清理,或者提供机制让子镜像清理。
|
||||
|
||||
---
|
||||
|
||||
## 本章小结
|
||||
|
||||
| 要点 | 说明 |
|
||||
|------|------|
|
||||
| **作用** | 定义在子镜像构建时执行的指令 |
|
||||
| **语法** | `ONBUILD INSTRUCTION` |
|
||||
| **适用** | 基础架构镜像(Node, Python, Go 等) |
|
||||
| **限制** | 只继承一次,不可级联 |
|
||||
| **规范** | 建议使用 `-onbuild` 标签后缀 |
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- [COPY 指令](copy.md):文件复制
|
||||
- [Dockerfile 最佳实践](../../15_appendix/best_practices.md):基础镜像设计
|
||||
7
04_image/dockerfile/references.md
Normal file
7
04_image/dockerfile/references.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# 参考文档
|
||||
|
||||
* `Dockerfile` 官方文档:https://docs.docker.com/engine/reference/builder/
|
||||
|
||||
* `Dockerfile` 最佳实践文档:https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
|
||||
|
||||
* `Docker` 官方镜像 `Dockerfile`:https://github.com/docker-library/docs
|
||||
181
04_image/dockerfile/run.md
Normal file
181
04_image/dockerfile/run.md
Normal file
@@ -0,0 +1,181 @@
|
||||
# RUN 执行命令
|
||||
|
||||
## 基本语法
|
||||
|
||||
```docker
|
||||
RUN <command>
|
||||
RUN ["executable", "param1", "param2"]
|
||||
```
|
||||
|
||||
`RUN` 指令是 Dockerfile 中最常用的指令之一。它在**当前镜像层**之上创建一个新层,执行指定的命令,并提交结果。
|
||||
|
||||
---
|
||||
|
||||
## 两种格式对比
|
||||
|
||||
### 1. Shell 格式
|
||||
|
||||
```docker
|
||||
RUN apt-get update
|
||||
```
|
||||
|
||||
- **特点**:默认通过 `/bin/sh -c` 执行。
|
||||
- **优势**:可以使用环境变量、管道、重定向等 Shell 特性。
|
||||
- **示例**:
|
||||
```docker
|
||||
RUN echo "Hello" > /test.txt
|
||||
```
|
||||
|
||||
### 2. Exec 格式
|
||||
|
||||
```docker
|
||||
RUN ["apt-get", "update"]
|
||||
```
|
||||
|
||||
- **特点**:直接调用可执行文件,不经过 Shell。
|
||||
- **优势**:避免 Shell 字符串解析问题,适用于参数中包含特殊字符的情况。
|
||||
- **注意**:无法使用 `$VAR` 环境变量替换(除非显式调用 shell)。
|
||||
|
||||
---
|
||||
|
||||
## 常见最佳实践
|
||||
|
||||
### 1. 组合命令(减少层数)
|
||||
|
||||
每一个 `RUN` 指令都会新建一层镜像。为了减少镜像体积和层数,应使用 `&&` 连接命令。
|
||||
|
||||
**❌ 糟糕的写法**(创建 3 层):
|
||||
|
||||
```docker
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y nginx
|
||||
RUN rm -rf /var/lib/apt/lists/*
|
||||
```
|
||||
|
||||
**✅ 推荐写法**(创建 1 层):
|
||||
|
||||
```docker
|
||||
RUN apt-get update && \
|
||||
apt-get install -y nginx && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
```
|
||||
|
||||
### 2. 清理缓存
|
||||
|
||||
在安装完软件后,立即清除缓存,可以显著减小镜像体积。
|
||||
|
||||
- **Debian/Ubuntu**:
|
||||
```docker
|
||||
RUN apt-get update && apt-get install -y package-bar \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
```
|
||||
- **Alpine**:
|
||||
```docker
|
||||
RUN apk add --no-cache package-bar
|
||||
```
|
||||
|
||||
### 3. 使用 `set -e` 和 `pipefail`
|
||||
|
||||
默认情况下,管道命令 `cmd1 | cmd2` 只要 `cmd2` 成功,整个 `RUN` 就视为成功。
|
||||
|
||||
**❌ 隐蔽的错误**:
|
||||
|
||||
```docker
|
||||
# 如果下载失败,gzip 可能会报错,但如果不影响后续,构建可能继续
|
||||
RUN wget http://error-url | gzip -d > file
|
||||
```
|
||||
|
||||
**✅ 推荐写法**:
|
||||
|
||||
```docker
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
RUN wget http://url | gzip -d > file
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 为什么 `RUN cd /app` 不生效?
|
||||
|
||||
```docker
|
||||
RUN cd /app
|
||||
RUN touch hello.txt
|
||||
```
|
||||
|
||||
**结果**:`hello.txt` 会出现在根目录 `/`,而不是 `/app`。
|
||||
|
||||
**原因**:每个 `RUN` 都在一个新的 Shell/容器 环境中执行。`cd` 只影响当前 `RUN` 的环境。
|
||||
|
||||
**解决**:使用 `WORKDIR` 指令。
|
||||
|
||||
```docker
|
||||
WORKDIR /app
|
||||
RUN touch hello.txt
|
||||
```
|
||||
|
||||
### Q: 环境变量不生效?
|
||||
|
||||
```docker
|
||||
RUN export MY_VAR=hello
|
||||
RUN echo $MY_VAR
|
||||
```
|
||||
|
||||
**结果**:输出为空。
|
||||
|
||||
**原因**:同上,环境变量只在当前 `RUN` 有效。
|
||||
|
||||
**解决**:使用 `ENV` 指令,或在同一行 `RUN` 中导出。
|
||||
|
||||
```docker
|
||||
ENV MY_VAR=hello
|
||||
RUN echo $MY_VAR
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 高级技巧
|
||||
|
||||
### 1. 使用 BuildKit 的挂载缓存
|
||||
|
||||
BuildKit 支持在 `RUN` 指令中使用 `--mount` 挂载缓存,加速构建。
|
||||
|
||||
```docker
|
||||
# 缓存 apt 包
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
apt-get update && apt-get install -y gcc
|
||||
```
|
||||
|
||||
```docker
|
||||
# 缓存 Go 模块
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
go build -o app
|
||||
```
|
||||
|
||||
### 2. 挂载密钥
|
||||
|
||||
安全地使用 SSH 密钥或 Token,而不将其记录在镜像中。
|
||||
|
||||
```docker
|
||||
RUN --mount=type=secret,id=mysecret \
|
||||
cat /run/secrets/mysecret
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 本章小结
|
||||
|
||||
| 要点 | 说明 |
|
||||
|------|------|
|
||||
| **作用** | 在新层执行命令 |
|
||||
| **原则** | 合并命令,清理缓存 |
|
||||
| **格式** | Shell (常用) vs Exec |
|
||||
| **陷阱** | `cd` 不持久,环境变量不持久 |
|
||||
| **进阶** | 使用 Cache Mount 加速构建 |
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- [CMD 容器启动命令](cmd.md):容器启动时的命令
|
||||
- [WORKDIR 指定工作目录](workdir.md):改变目录
|
||||
- [Dockerfile 最佳实践](../../15_appendix/best_practices.md)
|
||||
141
04_image/dockerfile/shell.md
Normal file
141
04_image/dockerfile/shell.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# SHELL 指令
|
||||
|
||||
## 基本语法
|
||||
|
||||
```docker
|
||||
SHELL ["executable", "parameters"]
|
||||
```
|
||||
|
||||
`SHELL` 指令允许覆盖 Docker 默认的 shell。
|
||||
- **Linux 默认**:`["/bin/sh", "-c"]`
|
||||
- **Windows 默认**:`["cmd", "/S", "/C"]`
|
||||
|
||||
该指令会影响后续的 `RUN`, `CMD`, `ENTRYPOINT` 指令(当它们使用 shell 格式时)。
|
||||
|
||||
---
|
||||
|
||||
## 为什么要用 SHELL 指令
|
||||
|
||||
### 1. 使用 bash 特性
|
||||
|
||||
默认的 `/bin/sh`(通常是 dash 或 alpine 的 ash)功能有限。如果你需要使用 bash 的特有功能(如数组、`{}` 扩展、`pipefail` 等),可以切换 shell。
|
||||
|
||||
```docker
|
||||
FROM ubuntu:24.04
|
||||
|
||||
# 切换到 bash
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
# 现在可以使用 bash 特性了
|
||||
RUN echo {a..z}
|
||||
```
|
||||
|
||||
### 2. 增强错误处理 (pipefail)
|
||||
|
||||
默认情况下,管道命令 `cmd1 | cmd2` 只要 `cmd2` 成功,整个指令就视为成功。这可能掩盖构建错误。
|
||||
|
||||
```docker
|
||||
# ❌ 这里的 wget 失败了,但构建继续(因为 tar 成功了)
|
||||
RUN wget -O - https://invalid-url | tar xz
|
||||
```
|
||||
|
||||
使用 `SHELL` 启用 `pipefail`:
|
||||
|
||||
```docker
|
||||
# ✅ 启用 pipefail
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
# 如果 wget 失败,整个 RUN 就会失败
|
||||
RUN wget -O - https://invalid-url | tar xz
|
||||
```
|
||||
|
||||
### 3. Windows 环境
|
||||
|
||||
在 Windows 容器中,经常需要在 `cmd` 和 `powershell` 之间切换。
|
||||
|
||||
```docker
|
||||
FROM mcr.microsoft.com/windows/servercore:ltsc2022
|
||||
|
||||
# 默认是 cmd
|
||||
RUN echo Default shell is cmd
|
||||
|
||||
# 切换到 powershell
|
||||
SHELL ["powershell", "-command"]
|
||||
RUN Write-Host "Hello from PowerShell"
|
||||
|
||||
# 切回 cmd
|
||||
SHELL ["cmd", "/S", "/C"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 作用范围
|
||||
|
||||
`SHELL` 指令可以出现多次,每次只影响其后的指令:
|
||||
|
||||
```docker
|
||||
FROM ubuntu:24.04
|
||||
|
||||
# 使用默认 sh
|
||||
RUN echo "Using sh"
|
||||
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
# 使用 bash
|
||||
RUN echo "Using bash"
|
||||
|
||||
SHELL ["/bin/sh", "-c"]
|
||||
# 回到 sh
|
||||
RUN echo "Using sh again"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 对其他指令的影响
|
||||
|
||||
`SHELL` 影响的是所有使用 **shell 格式** 的指令:
|
||||
|
||||
| 指令格式 | 是否受 SHELL 影响 |
|
||||
|---------|-------------------|
|
||||
| `RUN command` | ✅ 是 |
|
||||
| `RUN ["exec", "param"]` | ❌ 否 |
|
||||
| `CMD command` | ✅ 是 |
|
||||
| `CMD ["exec", "param"]` | ❌ 否 |
|
||||
| `ENTRYPOINT command` | ✅ 是 |
|
||||
| `ENTRYPOINT ["exec", "param"]` | ❌ 否 |
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 推荐开启 pipefail
|
||||
|
||||
对于使用 bash 的镜像,强烈建议开启 `pipefail`,以确保构建过程中的错误能被及时捕获。
|
||||
|
||||
```docker
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
```
|
||||
|
||||
### 2. 明确意图
|
||||
|
||||
如果由于脚本需求必须更改 shell,最好在 Dockerfile 中显式声明,而不是依赖默认行为。
|
||||
|
||||
### 3. 尽量保持一致
|
||||
|
||||
避免在 Dockerfile 中频繁切换 SHELL,这会使构建过程难以理解和调试。尽量在头部定义一次即可。
|
||||
|
||||
---
|
||||
|
||||
## 本章小结
|
||||
|
||||
| 要点 | 说明 |
|
||||
|------|------|
|
||||
| **作用** | 更改 RUN/CMD/ENTRYPOINT 的默认 shell |
|
||||
| **Linux 默认** | `["/bin/sh", "-c"]` |
|
||||
| **Windows 默认** | `["cmd", "/S", "/C"]` |
|
||||
| **推荐用法** | `SHELL ["/bin/bash", "-o", "pipefail", "-c"]` |
|
||||
| **影响范围** | 后续所有使用 shell 格式的指令 |
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- [RUN 指令](../../04_image/build.md):执行命令
|
||||
- [Dockerfile 最佳实践](../../15_appendix/best_practices.md):错误处理与调试
|
||||
273
04_image/dockerfile/user.md
Normal file
273
04_image/dockerfile/user.md
Normal file
@@ -0,0 +1,273 @@
|
||||
# USER 指定当前用户
|
||||
|
||||
## 基本语法
|
||||
|
||||
```docker
|
||||
USER <用户名>[:<用户组>]
|
||||
USER <UID>[:<GID>]
|
||||
```
|
||||
|
||||
`USER` 指令切换后续指令(RUN、CMD、ENTRYPOINT)的执行用户。
|
||||
|
||||
---
|
||||
|
||||
## 为什么要使用 USER
|
||||
|
||||
> 笔者强调:以非 root 用户运行容器是最重要的安全实践之一。
|
||||
|
||||
```
|
||||
root 用户运行的风险:
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ 容器内 root ←─ 可能逃逸 ─→ 宿主机 root │
|
||||
│ │ │ │
|
||||
│ └── 漏洞利用 ───────────────→ 完全控制宿主机 │
|
||||
└────────────────────────────────────────────────────────┘
|
||||
|
||||
非 root 用户运行:
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ 容器内普通用户 ──逃逸后──→ 宿主机普通用户 │
|
||||
│ │ │ │
|
||||
│ └── 权限受限,危害降低 ─────→ 无法控制系统 │
|
||||
└────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 创建并切换用户
|
||||
|
||||
```docker
|
||||
FROM node:20-alpine
|
||||
|
||||
# 1. 创建用户和组
|
||||
RUN addgroup -g 1001 appgroup && \
|
||||
adduser -u 1001 -G appgroup -D appuser
|
||||
|
||||
# 2. 设置目录权限
|
||||
WORKDIR /app
|
||||
COPY --chown=appuser:appgroup . .
|
||||
|
||||
# 3. 切换用户
|
||||
USER appuser
|
||||
|
||||
# 4. 后续命令以 appuser 身份运行
|
||||
CMD ["node", "server.js"]
|
||||
```
|
||||
|
||||
### 使用 UID/GID
|
||||
|
||||
```docker
|
||||
# 也可以使用数字
|
||||
USER 1001:1001
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 用户必须已存在
|
||||
|
||||
`USER` 指令只能切换到**已存在**的用户:
|
||||
|
||||
```docker
|
||||
# ❌ 错误:用户不存在
|
||||
USER nonexistent
|
||||
# Error: unable to find user nonexistent
|
||||
|
||||
# ✅ 正确:先创建用户
|
||||
RUN useradd -r -s /bin/false appuser
|
||||
USER appuser
|
||||
```
|
||||
|
||||
### 创建用户的方式
|
||||
|
||||
**Debian/Ubuntu**:
|
||||
|
||||
```docker
|
||||
RUN groupadd -r appgroup && \
|
||||
useradd -r -g appgroup appuser
|
||||
```
|
||||
|
||||
**Alpine**:
|
||||
|
||||
```docker
|
||||
RUN addgroup -g 1001 -S appgroup && \
|
||||
adduser -u 1001 -S -G appgroup appuser
|
||||
```
|
||||
|
||||
| 选项 | 说明 |
|
||||
|------|------|
|
||||
| `-r` (useradd) / `-S` (adduser) | 创建系统用户 |
|
||||
| `-g` | 指定主组 |
|
||||
| `-G` | 指定附加组 |
|
||||
| `-u` | 指定 UID |
|
||||
| `-s /bin/false` | 禁用登录 shell |
|
||||
|
||||
---
|
||||
|
||||
## 运行时切换用户
|
||||
|
||||
### 使用 gosu(推荐)
|
||||
|
||||
在 ENTRYPOINT 脚本中切换用户时,不要使用 `su` 或 `sudo`,应使用 [gosu](https://github.com/tianon/gosu):
|
||||
|
||||
```docker
|
||||
FROM debian:bookworm
|
||||
|
||||
# 创建用户
|
||||
RUN groupadd -r redis && useradd -r -g redis redis
|
||||
|
||||
# 安装 gosu
|
||||
RUN apt-get update && apt-get install -y gosu && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY docker-entrypoint.sh /usr/local/bin/
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||
CMD ["redis-server"]
|
||||
```
|
||||
|
||||
**docker-entrypoint.sh**:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# 以 root 执行初始化
|
||||
chown -R redis:redis /data
|
||||
|
||||
# 用 gosu 切换到 redis 用户运行服务
|
||||
exec gosu redis "$@"
|
||||
```
|
||||
|
||||
### 为什么不用 su/sudo
|
||||
|
||||
| 问题 | su/sudo | gosu |
|
||||
|------|---------|------|
|
||||
| TTY 要求 | 需要 | 不需要 |
|
||||
| 信号传递 | 不正确 | 正确 |
|
||||
| 子进程 | 是 | exec 替换 |
|
||||
| 容器中使用 | ❌ | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 运行时覆盖用户
|
||||
|
||||
使用 `-u` 或 `--user` 参数:
|
||||
|
||||
```bash
|
||||
# 以指定用户运行
|
||||
$ docker run -u 1001:1001 myimage
|
||||
|
||||
# 以 root 运行(调试时)
|
||||
$ docker run -u root myimage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 文件权限处理
|
||||
|
||||
切换用户后,确保应用有权访问文件:
|
||||
|
||||
```docker
|
||||
FROM node:20-alpine
|
||||
|
||||
# 创建用户
|
||||
RUN adduser -D -u 1001 appuser
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 方式1:使用 --chown
|
||||
COPY --chown=appuser:appuser . .
|
||||
|
||||
# 方式2:手动 chown(减少层数)
|
||||
# COPY . .
|
||||
# RUN chown -R appuser:appuser /app
|
||||
|
||||
USER appuser
|
||||
CMD ["node", "server.js"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 始终使用非 root 用户
|
||||
|
||||
```docker
|
||||
# ✅ 推荐
|
||||
RUN adduser -D appuser
|
||||
USER appuser
|
||||
CMD ["myapp"]
|
||||
|
||||
# ❌ 避免
|
||||
CMD ["myapp"] # 以 root 运行
|
||||
```
|
||||
|
||||
### 2. 使用固定 UID/GID
|
||||
|
||||
便于在宿主机和容器间共享文件:
|
||||
|
||||
```docker
|
||||
# 使用常见的非 root UID
|
||||
RUN addgroup -g 1000 -S appgroup && \
|
||||
adduser -u 1000 -S -G appgroup appuser
|
||||
USER 1000:1000
|
||||
```
|
||||
|
||||
### 3. 多阶段构建中的 USER
|
||||
|
||||
```docker
|
||||
# 构建阶段可以用 root
|
||||
FROM node:20 AS builder
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN npm install && npm run build
|
||||
|
||||
# 生产阶段用非 root
|
||||
FROM node:20-alpine
|
||||
RUN adduser -D appuser
|
||||
WORKDIR /app
|
||||
COPY --from=builder --chown=appuser:appuser /app/dist .
|
||||
USER appuser
|
||||
CMD ["node", "server.js"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 权限被拒绝
|
||||
|
||||
```bash
|
||||
permission denied: '/app/data.log'
|
||||
```
|
||||
|
||||
**解决**:确保目录权限正确
|
||||
|
||||
```docker
|
||||
RUN mkdir -p /app/data && chown appuser:appuser /app/data
|
||||
```
|
||||
|
||||
### Q: 无法绑定低于 1024 的端口
|
||||
|
||||
非 root 用户无法绑定 80、443 等端口。
|
||||
|
||||
**解决**:
|
||||
1. 使用高端口(如 8080)
|
||||
2. 在运行时映射端口:`docker run -p 80:8080`
|
||||
|
||||
---
|
||||
|
||||
## 本章小结
|
||||
|
||||
| 要点 | 说明 |
|
||||
|------|------|
|
||||
| **作用** | 切换后续指令的执行用户 |
|
||||
| **语法** | `USER username` 或 `USER UID:GID` |
|
||||
| **前提** | 用户必须已存在 |
|
||||
| **运行时覆盖** | `docker run -u` |
|
||||
| **切换工具** | 使用 gosu,不用 su/sudo |
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- [安全](../../security/README.md):容器安全实践
|
||||
- [ENTRYPOINT](entrypoint.md):入口脚本中的用户切换
|
||||
- [最佳实践](../../15_appendix/best_practices.md):Dockerfile 安全
|
||||
247
04_image/dockerfile/volume.md
Normal file
247
04_image/dockerfile/volume.md
Normal file
@@ -0,0 +1,247 @@
|
||||
# VOLUME 定义匿名卷
|
||||
|
||||
## 基本语法
|
||||
|
||||
```docker
|
||||
VOLUME ["/路径1", "/路径2"]
|
||||
VOLUME /路径
|
||||
```
|
||||
|
||||
`VOLUME` 指令创建挂载点,并标记为外部挂载的卷。
|
||||
|
||||
---
|
||||
|
||||
## 为什么使用 VOLUME
|
||||
|
||||
> **核心原则**:容器存储层应该保持无状态,任何运行时数据都应该存储在卷中。
|
||||
|
||||
```
|
||||
没有 VOLUME: 使用 VOLUME:
|
||||
┌─────────────────────┐ ┌─────────────────────┐
|
||||
│ 容器存储层 │ │ 容器存储层 │
|
||||
│ ┌─────────────┐ │ │ (只读/无状态) │
|
||||
│ │ 数据库文件 │←─问题 │ │
|
||||
│ │ 日志文件 │ │ └──────────┬──────────┘
|
||||
│ │ 上传文件 │ │ │
|
||||
│ └─────────────┘ │ ┌──────────▼──────────┐
|
||||
└─────────────────────┘ │ 数据卷 │
|
||||
容器删除 = 数据丢失 │ ┌─────────────┐ │
|
||||
│ │ 持久化数据 │←─安全
|
||||
│ └─────────────┘ │
|
||||
└─────────────────────┘
|
||||
容器删除,数据保留
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 定义单个卷
|
||||
|
||||
```docker
|
||||
FROM mysql:8.0
|
||||
VOLUME /var/lib/mysql
|
||||
```
|
||||
|
||||
### 定义多个卷
|
||||
|
||||
```docker
|
||||
FROM myapp
|
||||
VOLUME ["/data", "/logs", "/config"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## VOLUME 的行为
|
||||
|
||||
### 1. 自动创建匿名卷
|
||||
|
||||
如果运行时未指定挂载,Docker 会自动创建匿名卷:
|
||||
|
||||
```bash
|
||||
$ docker run mysql:8.0
|
||||
$ docker volume ls
|
||||
DRIVER VOLUME NAME
|
||||
local a1b2c3d4e5f6... # 自动创建的匿名卷
|
||||
```
|
||||
|
||||
### 2. 可被命名卷覆盖
|
||||
|
||||
```bash
|
||||
# 使用命名卷替代匿名卷
|
||||
$ docker run -v mysql_data:/var/lib/mysql mysql:8.0
|
||||
```
|
||||
|
||||
### 3. 可被 Bind Mount 覆盖
|
||||
|
||||
```bash
|
||||
# 使用宿主机目录替代
|
||||
$ docker run -v /my/data:/var/lib/mysql mysql:8.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## VOLUME 在构建时的特殊行为
|
||||
|
||||
> ⚠️ **重要**:VOLUME 之后对该目录的修改会被丢弃!
|
||||
|
||||
```docker
|
||||
FROM ubuntu
|
||||
VOLUME /data
|
||||
|
||||
# ❌ 这个文件不会出现在镜像中!
|
||||
RUN echo "hello" > /data/test.txt
|
||||
```
|
||||
|
||||
**原因**:VOLUME 指令之后,Docker 将该目录视为外部挂载点,不再记录对它的修改。
|
||||
|
||||
### 正确做法
|
||||
|
||||
```docker
|
||||
FROM ubuntu
|
||||
|
||||
# ✅ 先写入文件
|
||||
RUN mkdir -p /data && echo "hello" > /data/test.txt
|
||||
|
||||
# 再声明 VOLUME
|
||||
VOLUME /data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见使用场景
|
||||
|
||||
### 数据库持久化
|
||||
|
||||
```docker
|
||||
FROM postgres:15
|
||||
VOLUME /var/lib/postgresql/data
|
||||
```
|
||||
|
||||
### 日志目录
|
||||
|
||||
```docker
|
||||
FROM nginx
|
||||
VOLUME /var/log/nginx
|
||||
```
|
||||
|
||||
### 上传文件目录
|
||||
|
||||
```docker
|
||||
FROM myapp
|
||||
VOLUME /app/uploads
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 查看 VOLUME 定义
|
||||
|
||||
```bash
|
||||
# 查看镜像定义的 VOLUME
|
||||
$ docker inspect mysql:8.0 --format '{{json .Config.Volumes}}' | jq
|
||||
{
|
||||
"/var/lib/mysql": {}
|
||||
}
|
||||
|
||||
# 查看容器挂载的卷
|
||||
$ docker inspect mycontainer --format '{{json .Mounts}}' | jq
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## VOLUME vs docker run -v
|
||||
|
||||
| 特性 | Dockerfile VOLUME | docker run -v |
|
||||
|------|-------------------|---------------|
|
||||
| **定义时机** | 镜像构建时 | 容器运行时 |
|
||||
| **默认行为** | 创建匿名卷 | 可指定命名卷或路径 |
|
||||
| **灵活性** | 低(固定路径) | 高(可任意指定) |
|
||||
| **适用场景** | 定义必须持久化的路径 | 灵活的数据管理 |
|
||||
|
||||
---
|
||||
|
||||
## 在 Compose 中
|
||||
|
||||
```yaml
|
||||
services:
|
||||
db:
|
||||
image: postgres:15
|
||||
volumes:
|
||||
# 命名卷(推荐)
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
# Bind Mount
|
||||
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
|
||||
volumes:
|
||||
postgres_data: # 声明命名卷
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
### 匿名卷可能导致数据丢失
|
||||
|
||||
```bash
|
||||
# 使用 --rm 运行的容器,匿名卷会在容器删除时一起删除
|
||||
$ docker run --rm mysql:8.0
|
||||
# 容器停止后,数据丢失!
|
||||
```
|
||||
|
||||
**解决**:始终使用命名卷
|
||||
|
||||
```bash
|
||||
$ docker run -v mysql_data:/var/lib/mysql mysql:8.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 定义必须持久化的路径
|
||||
|
||||
```docker
|
||||
# 数据库必须使用卷
|
||||
FROM postgres:15
|
||||
VOLUME /var/lib/postgresql/data
|
||||
```
|
||||
|
||||
### 2. 不要在 VOLUME 后修改目录
|
||||
|
||||
```docker
|
||||
# ❌ 避免
|
||||
VOLUME /app/data
|
||||
RUN cp init-data.json /app/data/
|
||||
|
||||
# ✅ 正确
|
||||
RUN mkdir -p /app/data && cp init-data.json /app/data/
|
||||
VOLUME /app/data
|
||||
```
|
||||
|
||||
### 3. 文档中说明 VOLUME 用途
|
||||
|
||||
```docker
|
||||
# 持久化用户上传的文件
|
||||
VOLUME /app/uploads
|
||||
|
||||
# 持久化数据库数据
|
||||
VOLUME /var/lib/mysql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 本章小结
|
||||
|
||||
| 要点 | 说明 |
|
||||
|------|------|
|
||||
| **作用** | 创建挂载点,标记为外部卷 |
|
||||
| **语法** | `VOLUME /path` |
|
||||
| **默认行为** | 自动创建匿名卷 |
|
||||
| **覆盖方式** | `docker run -v name:/path` |
|
||||
| **注意** | VOLUME 之后的修改会丢失 |
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- [数据卷](../../07_data_network/data/volume.md):卷的管理和使用
|
||||
- [挂载主机目录](../../07_data_network/data/bind-mounts.md):Bind Mount
|
||||
- [Compose 数据管理](../../compose/compose_file.md):Compose 中的卷配置
|
||||
196
04_image/dockerfile/workdir.md
Normal file
196
04_image/dockerfile/workdir.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# WORKDIR 指定工作目录
|
||||
|
||||
## 基本语法
|
||||
|
||||
```docker
|
||||
WORKDIR <工作目录路径>
|
||||
```
|
||||
|
||||
`WORKDIR` 指定后续指令的工作目录。如果目录不存在,Docker 会自动创建。
|
||||
|
||||
---
|
||||
|
||||
## 基本用法
|
||||
|
||||
```docker
|
||||
WORKDIR /app
|
||||
|
||||
RUN pwd # 输出 /app
|
||||
RUN echo "hello" > world.txt # 创建 /app/world.txt
|
||||
COPY . . # 复制到 /app/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 为什么需要 WORKDIR
|
||||
|
||||
### 常见错误
|
||||
|
||||
```docker
|
||||
# ❌ 错误:cd 在下一个 RUN 中无效
|
||||
RUN cd /app
|
||||
RUN echo "hello" > world.txt # 文件在根目录!
|
||||
```
|
||||
|
||||
### 原因分析
|
||||
|
||||
```
|
||||
RUN cd /app
|
||||
↓
|
||||
启动容器 → cd /app(仅内存变化)→ 提交镜像层 → 容器销毁
|
||||
│
|
||||
↓ 工作目录未改变!
|
||||
RUN echo "hello" > world.txt
|
||||
↓
|
||||
启动新容器(工作目录在 /)→ 创建 /world.txt
|
||||
```
|
||||
|
||||
每个 RUN 都在新容器中执行,**前一个 RUN 的内存状态(包括工作目录)不会保留**。
|
||||
|
||||
### 正确做法
|
||||
|
||||
```docker
|
||||
# ✅ 正确:使用 WORKDIR
|
||||
WORKDIR /app
|
||||
RUN echo "hello" > world.txt # 创建 /app/world.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 相对路径
|
||||
|
||||
WORKDIR 支持相对路径,基于上一个 WORKDIR:
|
||||
|
||||
```docker
|
||||
WORKDIR /a
|
||||
WORKDIR b
|
||||
WORKDIR c
|
||||
|
||||
RUN pwd # 输出 /a/b/c
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用环境变量
|
||||
|
||||
```docker
|
||||
ENV APP_HOME=/app
|
||||
WORKDIR $APP_HOME
|
||||
|
||||
RUN pwd # 输出 /app
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 多阶段构建中的 WORKDIR
|
||||
|
||||
```docker
|
||||
# 构建阶段
|
||||
FROM node:20 AS builder
|
||||
WORKDIR /build
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# 生产阶段
|
||||
FROM nginx:alpine
|
||||
WORKDIR /usr/share/nginx/html
|
||||
COPY --from=builder /build/dist .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 尽早设置 WORKDIR
|
||||
|
||||
```docker
|
||||
FROM node:20
|
||||
WORKDIR /app # 尽早设置
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
CMD ["node", "server.js"]
|
||||
```
|
||||
|
||||
### 2. 使用绝对路径
|
||||
|
||||
```docker
|
||||
# ✅ 推荐:绝对路径,意图明确
|
||||
WORKDIR /app
|
||||
|
||||
# ⚠️ 避免:相对路径可能造成混淆
|
||||
WORKDIR app
|
||||
```
|
||||
|
||||
### 3. 不要用 RUN cd
|
||||
|
||||
```docker
|
||||
# ❌ 避免
|
||||
RUN cd /app && echo "hello" > world.txt
|
||||
|
||||
# ✅ 推荐
|
||||
WORKDIR /app
|
||||
RUN echo "hello" > world.txt
|
||||
```
|
||||
|
||||
### 4. 适时重置 WORKDIR
|
||||
|
||||
```docker
|
||||
WORKDIR /app
|
||||
# ... 应用相关操作 ...
|
||||
|
||||
WORKDIR /data
|
||||
# ... 数据相关操作 ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 与其他指令的关系
|
||||
|
||||
| 指令 | WORKDIR 的影响 |
|
||||
|------|---------------|
|
||||
| `RUN` | 在 WORKDIR 中执行命令 |
|
||||
| `CMD` | 在 WORKDIR 中启动 |
|
||||
| `ENTRYPOINT` | 在 WORKDIR 中启动 |
|
||||
| `COPY` | 相对目标路径基于 WORKDIR |
|
||||
| `ADD` | 相对目标路径基于 WORKDIR |
|
||||
|
||||
```docker
|
||||
WORKDIR /app
|
||||
|
||||
RUN pwd # /app
|
||||
COPY . . # 复制到 /app
|
||||
CMD ["./start.sh"] # /app/start.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 运行时覆盖
|
||||
|
||||
使用 `-w` 参数覆盖工作目录:
|
||||
|
||||
```bash
|
||||
$ docker run -w /tmp myimage pwd
|
||||
/tmp
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 本章小结
|
||||
|
||||
| 要点 | 说明 |
|
||||
|------|------|
|
||||
| **作用** | 设置后续指令的工作目录 |
|
||||
| **语法** | `WORKDIR /path` |
|
||||
| **自动创建** | 目录不存在会自动创建 |
|
||||
| **持久性** | 影响后续所有指令,直到下次 WORKDIR |
|
||||
| **不要用** | `RUN cd /path`(无效) |
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- [COPY 复制文件](copy.md):文件复制
|
||||
- [RUN 执行命令](../../04_image/build.md):执行构建命令
|
||||
- [最佳实践](../../15_appendix/best_practices.md):Dockerfile 编写指南
|
||||
9
04_image/internal.md
Normal file
9
04_image/internal.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# 镜像的实现原理
|
||||
|
||||
Docker 镜像是怎么实现增量的修改和维护的?
|
||||
|
||||
每个镜像都由很多层次构成,Docker 使用 [Union FS](https://en.wikipedia.org/wiki/UnionFS) 将这些不同的层结合到一个镜像中去。
|
||||
|
||||
通常 Union FS 有两个用途, 一方面可以实现不借助 LVM、RAID 将多个 disk 挂到同一个目录下,另一个更常用的就是将一个只读的分支和一个可写的分支联合在一起,Live CD 正是基于此方法可以允许在镜像不变的基础上允许用户在其上进行一些写操作。
|
||||
|
||||
Docker 在 OverlayFS 上构建的容器也是利用了类似的原理。
|
||||
258
04_image/list.md
Normal file
258
04_image/list.md
Normal file
@@ -0,0 +1,258 @@
|
||||
# 列出镜像
|
||||
|
||||
## 基本用法
|
||||
|
||||
查看本地已下载的镜像:
|
||||
|
||||
```bash
|
||||
$ docker image ls
|
||||
REPOSITORY TAG IMAGE ID CREATED SIZE
|
||||
redis latest 5f515359c7f8 5 days ago 183MB
|
||||
nginx latest 05a60462f8ba 5 days ago 181MB
|
||||
ubuntu 24.04 329ed837d508 3 days ago 78MB
|
||||
ubuntu noble 329ed837d508 3 days ago 78MB
|
||||
```
|
||||
|
||||
> 💡 `docker images` 是 `docker image ls` 的简写,两者等效。
|
||||
|
||||
---
|
||||
|
||||
## 输出字段说明
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| **REPOSITORY** | 仓库名 |
|
||||
| **TAG** | 标签(版本) |
|
||||
| **IMAGE ID** | 镜像唯一标识(短 ID,前 12 位) |
|
||||
| **CREATED** | 创建时间 |
|
||||
| **SIZE** | 本地占用空间 |
|
||||
|
||||
### 同一镜像多个标签
|
||||
|
||||
注意上面的 `ubuntu:24.04` 和 `ubuntu:noble` 拥有相同的 IMAGE ID——它们是同一个镜像的不同标签,只占用一份存储空间。
|
||||
|
||||
---
|
||||
|
||||
## 理解镜像大小
|
||||
|
||||
### 本地大小 vs Hub 显示大小
|
||||
|
||||
| 位置 | 显示大小 | 说明 |
|
||||
|------|---------|------|
|
||||
| Docker Hub | 29MB | 压缩后的网络传输大小 |
|
||||
| docker image ls | 78MB | 本地解压后的实际大小 |
|
||||
|
||||
### 实际磁盘占用
|
||||
|
||||
由于镜像是分层存储,不同镜像可能共享相同的层:
|
||||
|
||||
```
|
||||
ubuntu:24.04 nginx:latest redis:latest
|
||||
│ │ │
|
||||
└───────┬───────┘ │
|
||||
▼ │
|
||||
共享基础层 ◄───────────────────┘
|
||||
```
|
||||
|
||||
因此,`docker image ls` 中各镜像大小之和 > 实际磁盘占用。
|
||||
|
||||
### 查看实际空间占用
|
||||
|
||||
```bash
|
||||
$ docker system df
|
||||
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
|
||||
Images 15 3 2.5GB 1.8GB (72%)
|
||||
Containers 5 2 100MB 80MB (80%)
|
||||
Local Volumes 8 2 500MB 400MB (80%)
|
||||
Build Cache 0 0 0B 0B
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 过滤镜像
|
||||
|
||||
### 按仓库名过滤
|
||||
|
||||
```bash
|
||||
# 列出所有 ubuntu 镜像
|
||||
$ docker images ubuntu
|
||||
REPOSITORY TAG IMAGE ID SIZE
|
||||
ubuntu 24.04 329ed837d508 78MB
|
||||
ubuntu noble 329ed837d508 78MB
|
||||
ubuntu 22.04 a1b2c3d4e5f6 72MB
|
||||
```
|
||||
|
||||
### 按仓库名和标签过滤
|
||||
|
||||
```bash
|
||||
$ docker images ubuntu:24.04
|
||||
REPOSITORY TAG IMAGE ID SIZE
|
||||
ubuntu 24.04 329ed837d508 78MB
|
||||
```
|
||||
|
||||
### 使用过滤器 --filter
|
||||
|
||||
| 过滤条件 | 说明 | 示例 |
|
||||
|---------|------|------|
|
||||
| `dangling=true` | 虚悬镜像 | `-f dangling=true` |
|
||||
| `before=镜像` | 在某镜像之前创建 | `-f before=nginx:latest` |
|
||||
| `since=镜像` | 在某镜像之后创建 | `-f since=nginx:latest` |
|
||||
| `label=key=value` | 按 LABEL 过滤 | `-f label=version=1.0` |
|
||||
| `reference=pattern` | 按名称模式 | `-f reference='*:latest'` |
|
||||
|
||||
```bash
|
||||
# 列出 nginx 之后创建的镜像
|
||||
$ docker images -f since=nginx:latest
|
||||
|
||||
# 列出所有带 latest 标签的镜像
|
||||
$ docker images -f reference='*:latest'
|
||||
|
||||
# 列出带特定 LABEL 的镜像
|
||||
$ docker images -f label=maintainer=example@email.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 虚悬镜像(Dangling Images)
|
||||
|
||||
### 什么是虚悬镜像
|
||||
|
||||
仓库名和标签都显示为 `<none>` 的镜像:
|
||||
|
||||
```bash
|
||||
$ docker images
|
||||
REPOSITORY TAG IMAGE ID SIZE
|
||||
<none> <none> 00285df0df87 342MB
|
||||
```
|
||||
|
||||
### 产生原因
|
||||
|
||||
1. **镜像重新构建**:新镜像使用了旧镜像的标签,旧镜像标签被移除
|
||||
2. **docker pull 更新**:拉取更新版本时,旧版本失去标签
|
||||
|
||||
### 处理虚悬镜像
|
||||
|
||||
```bash
|
||||
# 列出虚悬镜像
|
||||
$ docker images -f dangling=true
|
||||
|
||||
# 删除虚悬镜像
|
||||
$ docker image prune
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 中间层镜像
|
||||
|
||||
### 查看所有镜像(包含中间层)
|
||||
|
||||
```bash
|
||||
$ docker images -a
|
||||
```
|
||||
|
||||
会显示很多无标签镜像——这些是构建过程中产生的中间层,被其他镜像依赖。
|
||||
|
||||
> ⚠️ 不要删除中间层镜像。它们是其他镜像的依赖,删除会导致上层镜像无法使用。删除顶层镜像时会自动清理不再需要的中间层。
|
||||
|
||||
---
|
||||
|
||||
## 格式化输出
|
||||
|
||||
### 只输出 ID
|
||||
|
||||
```bash
|
||||
$ docker images -q
|
||||
5f515359c7f8
|
||||
05a60462f8ba
|
||||
329ed837d508
|
||||
```
|
||||
|
||||
常用于配合其他命令:
|
||||
|
||||
```bash
|
||||
# 删除所有镜像
|
||||
$ docker rmi $(docker images -q)
|
||||
|
||||
# 删除所有 redis 镜像
|
||||
$ docker rmi $(docker images -q redis)
|
||||
```
|
||||
|
||||
### 显示完整 ID
|
||||
|
||||
```bash
|
||||
$ docker images --no-trunc
|
||||
```
|
||||
|
||||
### 显示摘要
|
||||
|
||||
```bash
|
||||
$ docker images --digests
|
||||
REPOSITORY TAG DIGEST IMAGE ID
|
||||
nginx latest sha256:b4f0e0bdeb5... e43d811ce2f4
|
||||
```
|
||||
|
||||
### 自定义格式
|
||||
|
||||
使用 Go 模板语法自定义输出:
|
||||
|
||||
```bash
|
||||
# 只显示 ID 和仓库名
|
||||
$ docker images --format "{{.ID}}: {{.Repository}}"
|
||||
5f515359c7f8: redis
|
||||
05a60462f8ba: nginx
|
||||
329ed837d508: ubuntu
|
||||
|
||||
# 表格形式(带标题)
|
||||
$ docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
|
||||
REPOSITORY TAG SIZE
|
||||
redis latest 183MB
|
||||
nginx latest 181MB
|
||||
ubuntu 24.04 78MB
|
||||
```
|
||||
|
||||
### 可用模板字段
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `.ID` | 镜像 ID |
|
||||
| `.Repository` | 仓库名 |
|
||||
| `.Tag` | 标签 |
|
||||
| `.Digest` | 摘要 |
|
||||
| `.CreatedSince` | 创建后经过的时间 |
|
||||
| `.CreatedAt` | 创建时间 |
|
||||
| `.Size` | 大小 |
|
||||
|
||||
---
|
||||
|
||||
## 常用命令组合
|
||||
|
||||
```bash
|
||||
# 列出所有镜像及其大小,按大小排序(需要系统 sort 命令)
|
||||
$ docker images --format "{{.Size}}\t{{.Repository}}:{{.Tag}}" | sort -h
|
||||
|
||||
# 查找大于 500MB 的镜像
|
||||
$ docker images --format "{{.Size}}\t{{.Repository}}:{{.Tag}}" | grep -E "^[0-9]+GB|^[5-9][0-9]{2}MB"
|
||||
|
||||
# 导出镜像列表
|
||||
$ docker images --format "{{.Repository}}:{{.Tag}}" > images.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 本章小结
|
||||
|
||||
| 操作 | 命令 |
|
||||
|------|------|
|
||||
| 列出所有镜像 | `docker images` |
|
||||
| 按仓库名过滤 | `docker images nginx` |
|
||||
| 列出虚悬镜像 | `docker images -f dangling=true` |
|
||||
| 只输出 ID | `docker images -q` |
|
||||
| 显示摘要 | `docker images --digests` |
|
||||
| 自定义格式 | `docker images --format "..."` |
|
||||
| 查看空间占用 | `docker system df` |
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- [获取镜像](pull.md):从 Registry 拉取镜像
|
||||
- [删除镜像](rm.md):清理本地镜像
|
||||
- [镜像](../02_basic_concept/image.md):理解镜像概念
|
||||
192
04_image/multistage-builds/README.md
Normal file
192
04_image/multistage-builds/README.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# 多阶段构建
|
||||
|
||||
## 之前的做法
|
||||
|
||||
在 Docker 17.05 版本之前,我们构建 Docker 镜像时,通常会采用两种方式:
|
||||
|
||||
### 全部放入一个 Dockerfile
|
||||
|
||||
一种方式是将所有的构建过程编包含在一个 `Dockerfile` 中,包括项目及其依赖库的编译、测试、打包等流程,这里可能会带来的一些问题:
|
||||
|
||||
* 镜像层次多,镜像体积较大,部署时间变长
|
||||
|
||||
* 源代码存在泄露的风险
|
||||
|
||||
例如,编写 `app.go` 文件,该程序输出 `Hello World!`
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main(){
|
||||
fmt.Printf("Hello World!");
|
||||
}
|
||||
```
|
||||
|
||||
编写 `Dockerfile.one` 文件
|
||||
|
||||
```docker
|
||||
FROM golang:alpine
|
||||
|
||||
RUN apk --no-cache add git ca-certificates
|
||||
|
||||
WORKDIR /go/src/github.com/go/helloworld/
|
||||
|
||||
COPY app.go .
|
||||
|
||||
RUN go mod init helloworld \
|
||||
&& go get -d -v github.com/go-sql-driver/mysql \
|
||||
&& CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . \
|
||||
&& cp /go/src/github.com/go/helloworld/app /root
|
||||
|
||||
WORKDIR /root/
|
||||
|
||||
CMD ["./app"]
|
||||
```
|
||||
|
||||
构建镜像
|
||||
|
||||
```bash
|
||||
$ docker build -t go/helloworld:1 -f Dockerfile.one .
|
||||
```
|
||||
|
||||
### 分散到多个 Dockerfile
|
||||
|
||||
另一种方式,就是我们事先在一个 `Dockerfile` 将项目及其依赖库编译测试打包好后,再将其拷贝到运行环境中,这种方式需要我们编写两个 `Dockerfile` 和一些编译脚本才能将其两个阶段自动整合起来,这种方式虽然可以很好地规避第一种方式存在的风险,但明显部署过程较复杂。
|
||||
|
||||
例如,编写 `Dockerfile.build` 文件
|
||||
|
||||
```docker
|
||||
FROM golang:alpine
|
||||
|
||||
RUN apk --no-cache add git
|
||||
|
||||
WORKDIR /go/src/github.com/go/helloworld
|
||||
|
||||
COPY app.go .
|
||||
|
||||
RUN go get -d -v github.com/go-sql-driver/mysql \
|
||||
&& CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
|
||||
```
|
||||
|
||||
编写 `Dockerfile.copy` 文件
|
||||
|
||||
```docker
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk --no-cache add ca-certificates
|
||||
|
||||
WORKDIR /root/
|
||||
|
||||
COPY app .
|
||||
|
||||
CMD ["./app"]
|
||||
```
|
||||
|
||||
新建 `build.sh`
|
||||
|
||||
```bash
|
||||
#!/bin/sh
|
||||
echo Building go/helloworld:build
|
||||
|
||||
docker build -t go/helloworld:build . -f Dockerfile.build
|
||||
|
||||
docker create --name extract go/helloworld:build
|
||||
docker cp extract:/go/src/github.com/go/helloworld/app ./app
|
||||
docker rm -f extract
|
||||
|
||||
echo Building go/helloworld:2
|
||||
|
||||
docker build --no-cache -t go/helloworld:2 . -f Dockerfile.copy
|
||||
rm ./app
|
||||
```
|
||||
|
||||
现在运行脚本即可构建镜像
|
||||
|
||||
```bash
|
||||
$ chmod +x build.sh
|
||||
|
||||
$ ./build.sh
|
||||
```
|
||||
|
||||
对比两种方式生成的镜像大小
|
||||
|
||||
```bash
|
||||
$ docker image ls
|
||||
|
||||
REPOSITORY TAG IMAGE ID CREATED SIZE
|
||||
go/helloworld 2 f7cf3465432c 22 seconds ago 6.47MB
|
||||
go/helloworld 1 f55d3e16affc 2 minutes ago 295MB
|
||||
```
|
||||
|
||||
## 使用多阶段构建
|
||||
|
||||
为解决以上问题,Docker v17.05 开始支持多阶段构建 (`multistage builds`)。使用多阶段构建我们就可以很容易解决前面提到的问题,并且只需要编写一个 `Dockerfile`:
|
||||
|
||||
例如,编写 `Dockerfile` 文件
|
||||
|
||||
```docker
|
||||
FROM golang:alpine as builder
|
||||
|
||||
RUN apk --no-cache add git
|
||||
|
||||
WORKDIR /go/src/github.com/go/helloworld/
|
||||
|
||||
RUN go get -d -v github.com/go-sql-driver/mysql
|
||||
|
||||
COPY app.go .
|
||||
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
|
||||
|
||||
FROM alpine:latest as prod
|
||||
|
||||
RUN apk --no-cache add ca-certificates
|
||||
|
||||
WORKDIR /root/
|
||||
|
||||
COPY --from=0 /go/src/github.com/go/helloworld/app .
|
||||
|
||||
CMD ["./app"]
|
||||
```
|
||||
|
||||
构建镜像
|
||||
|
||||
```bash
|
||||
$ docker build -t go/helloworld:3 .
|
||||
```
|
||||
|
||||
对比三个镜像大小
|
||||
|
||||
```bash
|
||||
$ docker image ls
|
||||
|
||||
REPOSITORY TAG IMAGE ID CREATED SIZE
|
||||
go/helloworld 3 d6911ed9c846 7 seconds ago 6.47MB
|
||||
go/helloworld 2 f7cf3465432c 22 seconds ago 6.47MB
|
||||
go/helloworld 1 f55d3e16affc 2 minutes ago 295MB
|
||||
```
|
||||
|
||||
很明显使用多阶段构建的镜像体积小,同时也完美解决了上边提到的问题。
|
||||
|
||||
### 只构建某一阶段的镜像
|
||||
|
||||
我们可以使用 `as` 来为某一阶段命名,例如
|
||||
|
||||
```docker
|
||||
FROM golang:alpine as builder
|
||||
```
|
||||
|
||||
例如当我们只想构建 `builder` 阶段的镜像时,增加 `--target=builder` 参数即可
|
||||
|
||||
```bash
|
||||
$ docker build --target builder -t username/imagename:tag .
|
||||
```
|
||||
|
||||
### 构建时从其他镜像复制文件
|
||||
|
||||
上面例子中我们使用 `COPY --from=0 /go/src/github.com/go/helloworld/app .` 从上一阶段的镜像中复制文件,我们也可以复制任意镜像中的文件。
|
||||
|
||||
```docker
|
||||
COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf
|
||||
```
|
||||
17
04_image/multistage-builds/example/laravel/.dockerignore
Normal file
17
04_image/multistage-builds/example/laravel/.dockerignore
Normal file
@@ -0,0 +1,17 @@
|
||||
.idea/
|
||||
.git/
|
||||
|
||||
vendor/
|
||||
|
||||
node_modules/
|
||||
|
||||
public/js/
|
||||
public/css/
|
||||
public/mix-manifest.json
|
||||
|
||||
yarn-error.log
|
||||
|
||||
bootstrap/cache/*
|
||||
storage/
|
||||
|
||||
# 自行添加其他需要排除的文件,例如 .env.* 文件
|
||||
55
04_image/multistage-builds/example/laravel/Dockerfile
Normal file
55
04_image/multistage-builds/example/laravel/Dockerfile
Normal file
@@ -0,0 +1,55 @@
|
||||
FROM node:alpine as frontend
|
||||
|
||||
COPY package.json /app/
|
||||
|
||||
RUN set -x ; cd /app \
|
||||
&& npm install --registry=https://registry.npm.taobao.org
|
||||
|
||||
COPY webpack.mix.js webpack.config.js tailwind.config.js /app/
|
||||
COPY resources/ /app/resources/
|
||||
|
||||
RUN set -x ; cd /app \
|
||||
&& touch artisan \
|
||||
&& mkdir -p public \
|
||||
&& npm run production
|
||||
|
||||
FROM composer as composer
|
||||
|
||||
COPY database/ /app/database/
|
||||
COPY composer.json /app/
|
||||
|
||||
RUN set -x ; cd /app \
|
||||
&& composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/ \
|
||||
&& composer install \
|
||||
--ignore-platform-reqs \
|
||||
--no-interaction \
|
||||
--no-plugins \
|
||||
--no-scripts \
|
||||
--prefer-dist
|
||||
|
||||
FROM php:7.4-fpm-alpine as laravel
|
||||
|
||||
ARG LARAVEL_PATH=/app/laravel
|
||||
|
||||
COPY --from=composer /app/vendor/ ${LARAVEL_PATH}/vendor/
|
||||
COPY . ${LARAVEL_PATH}
|
||||
COPY --from=frontend /app/public/js/ ${LARAVEL_PATH}/public/js/
|
||||
COPY --from=frontend /app/public/css/ ${LARAVEL_PATH}/public/css/
|
||||
COPY --from=frontend /app/public/mix-manifest.json ${LARAVEL_PATH}/public/mix-manifest.json
|
||||
|
||||
RUN set -x ; cd ${LARAVEL_PATH} \
|
||||
&& mkdir -p storage \
|
||||
&& mkdir -p storage/framework/cache \
|
||||
&& mkdir -p storage/framework/sessions \
|
||||
&& mkdir -p storage/framework/testing \
|
||||
&& mkdir -p storage/framework/views \
|
||||
&& mkdir -p storage/logs \
|
||||
&& chmod -R 777 storage \
|
||||
&& php artisan package:discover
|
||||
|
||||
FROM nginx:alpine as nginx
|
||||
|
||||
ARG LARAVEL_PATH=/app/laravel
|
||||
|
||||
COPY laravel.conf /etc/nginx/conf.d/
|
||||
COPY --from=laravel ${LARAVEL_PATH}/public ${LARAVEL_PATH}/public
|
||||
18
04_image/multistage-builds/example/laravel/laravel.conf
Normal file
18
04_image/multistage-builds/example/laravel/laravel.conf
Normal file
@@ -0,0 +1,18 @@
|
||||
server {
|
||||
listen 80 default_server;
|
||||
root /app/laravel/public;
|
||||
index index.php index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location ~ .*\.php(\/.*)*$ {
|
||||
fastcgi_pass laravel:9000;
|
||||
include fastcgi.conf;
|
||||
|
||||
# fastcgi_connect_timeout 300;
|
||||
# fastcgi_send_timeout 300;
|
||||
# fastcgi_read_timeout 300;
|
||||
}
|
||||
}
|
||||
232
04_image/multistage-builds/laravel.md
Normal file
232
04_image/multistage-builds/laravel.md
Normal file
@@ -0,0 +1,232 @@
|
||||
# 实战多阶段构建 Laravel 镜像
|
||||
|
||||
> 本节适用于 PHP 开发者阅读。`Laravel` 基于 8.x 版本,各个版本的文件结构可能会有差异,请根据实际自行修改。
|
||||
|
||||
## 准备
|
||||
|
||||
新建一个 `Laravel` 项目或在已有的 `Laravel` 项目根目录下新建 `Dockerfile` `.dockerignore` `laravel.conf` 文件。
|
||||
|
||||
在 `.dockerignore` 文件中写入以下内容。
|
||||
|
||||
```bash
|
||||
.idea/
|
||||
.git/
|
||||
|
||||
vendor/
|
||||
|
||||
node_modules/
|
||||
|
||||
public/js/
|
||||
public/css/
|
||||
public/mix-manifest.json
|
||||
|
||||
yarn-error.log
|
||||
|
||||
bootstrap/cache/*
|
||||
storage/
|
||||
|
||||
# 自行添加其他需要排除的文件,例如 .env.* 文件
|
||||
```
|
||||
|
||||
在 `laravel.conf` 文件中写入 nginx 配置。
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80 default_server;
|
||||
root /app/laravel/public;
|
||||
index index.php index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location ~ .*\.php(\/.*)*$ {
|
||||
fastcgi_pass laravel:9000;
|
||||
include fastcgi.conf;
|
||||
|
||||
# fastcgi_connect_timeout 300;
|
||||
# fastcgi_send_timeout 300;
|
||||
# fastcgi_read_timeout 300;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 前端构建
|
||||
|
||||
第一阶段进行前端构建。
|
||||
|
||||
```docker
|
||||
FROM node:alpine as frontend
|
||||
|
||||
COPY package.json /app/
|
||||
|
||||
RUN set -x ; cd /app \
|
||||
&& npm install --registry=https://registry.npmmirror.com
|
||||
|
||||
COPY webpack.mix.js webpack.config.js tailwind.config.js /app/
|
||||
COPY resources/ /app/resources/
|
||||
|
||||
RUN set -x ; cd /app \
|
||||
&& touch artisan \
|
||||
&& mkdir -p public \
|
||||
&& npm run production
|
||||
```
|
||||
|
||||
## 安装 Composer 依赖
|
||||
|
||||
第二阶段安装 Composer 依赖。
|
||||
|
||||
```docker
|
||||
FROM composer as composer
|
||||
|
||||
COPY database/ /app/database/
|
||||
COPY composer.json composer.lock /app/
|
||||
|
||||
RUN set -x ; cd /app \
|
||||
&& composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/ \
|
||||
&& composer install \
|
||||
--ignore-platform-reqs \
|
||||
--no-interaction \
|
||||
--no-plugins \
|
||||
--no-scripts \
|
||||
--prefer-dist
|
||||
```
|
||||
|
||||
## 整合以上阶段所生成的文件
|
||||
|
||||
第三阶段对以上阶段生成的文件进行整合。
|
||||
|
||||
```docker
|
||||
FROM php:7.4-fpm-alpine as laravel
|
||||
|
||||
ARG LARAVEL_PATH=/app/laravel
|
||||
|
||||
COPY --from=composer /app/vendor/ ${LARAVEL_PATH}/vendor/
|
||||
COPY . ${LARAVEL_PATH}
|
||||
COPY --from=frontend /app/public/js/ ${LARAVEL_PATH}/public/js/
|
||||
COPY --from=frontend /app/public/css/ ${LARAVEL_PATH}/public/css/
|
||||
COPY --from=frontend /app/public/mix-manifest.json ${LARAVEL_PATH}/public/mix-manifest.json
|
||||
|
||||
RUN set -x ; cd ${LARAVEL_PATH} \
|
||||
&& mkdir -p storage \
|
||||
&& mkdir -p storage/framework/cache \
|
||||
&& mkdir -p storage/framework/sessions \
|
||||
&& mkdir -p storage/framework/testing \
|
||||
&& mkdir -p storage/framework/views \
|
||||
&& mkdir -p storage/logs \
|
||||
&& chmod -R 777 storage \
|
||||
&& php artisan package:discover
|
||||
```
|
||||
|
||||
## 最后一个阶段构建 NGINX 镜像
|
||||
|
||||
```docker
|
||||
FROM nginx:alpine as nginx
|
||||
|
||||
ARG LARAVEL_PATH=/app/laravel
|
||||
|
||||
COPY laravel.conf /etc/nginx/conf.d/
|
||||
COPY --from=laravel ${LARAVEL_PATH}/public ${LARAVEL_PATH}/public
|
||||
```
|
||||
|
||||
## 构建 Laravel 及 Nginx 镜像
|
||||
|
||||
使用 `docker build` 命令构建镜像。
|
||||
|
||||
```bash
|
||||
$ docker build -t my/laravel --target=laravel .
|
||||
|
||||
$ docker build -t my/nginx --target=nginx .
|
||||
```
|
||||
|
||||
## 启动容器并测试
|
||||
|
||||
新建 Docker 网络
|
||||
|
||||
```bash
|
||||
$ docker network create laravel
|
||||
```
|
||||
|
||||
启动 laravel 容器, `--name=laravel` 参数设定的名字必须与 `nginx` 配置文件中的 `fastcgi_pass laravel:9000;` 一致
|
||||
|
||||
```bash
|
||||
$ docker run -dit --rm --name=laravel --network=laravel my/laravel
|
||||
```
|
||||
|
||||
启动 nginx 容器
|
||||
|
||||
```bash
|
||||
$ docker run -dit --rm --network=laravel -p 8080:80 my/nginx
|
||||
```
|
||||
|
||||
浏览器访问 `127.0.0.1:8080` 可以看到 Laravel 项目首页。
|
||||
|
||||
> 也许 Laravel 项目依赖其他外部服务,例如 redis、MySQL,请自行启动这些服务之后再进行测试,本小节不再赘述。
|
||||
|
||||
## 生产环境优化
|
||||
|
||||
本小节内容为了方便测试,将配置文件直接放到了镜像中,实际在使用时 **建议** 将配置文件作为 `config` 或 `secret` 挂载到容器中,请读者自行学习 `Kubernetes` 的相关内容。
|
||||
|
||||
由于篇幅所限本小节只是简单列出,更多内容可以参考 https://github.com/khs1994-docker/laravel-demo 项目。
|
||||
|
||||
## 附录
|
||||
|
||||
完整的 `Dockerfile` 文件如下。
|
||||
|
||||
```docker
|
||||
FROM node:alpine as frontend
|
||||
|
||||
COPY package.json /app/
|
||||
|
||||
RUN set -x ; cd /app \
|
||||
&& npm install --registry=https://registry.npmmirror.com
|
||||
|
||||
COPY webpack.mix.js webpack.config.js tailwind.config.js /app/
|
||||
COPY resources/ /app/resources/
|
||||
|
||||
RUN set -x ; cd /app \
|
||||
&& touch artisan \
|
||||
&& mkdir -p public \
|
||||
&& npm run production
|
||||
|
||||
FROM composer as composer
|
||||
|
||||
COPY database/ /app/database/
|
||||
COPY composer.json /app/
|
||||
|
||||
RUN set -x ; cd /app \
|
||||
&& composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/ \
|
||||
&& composer install \
|
||||
--ignore-platform-reqs \
|
||||
--no-interaction \
|
||||
--no-plugins \
|
||||
--no-scripts \
|
||||
--prefer-dist
|
||||
|
||||
FROM php:7.4-fpm-alpine as laravel
|
||||
|
||||
ARG LARAVEL_PATH=/app/laravel
|
||||
|
||||
COPY --from=composer /app/vendor/ ${LARAVEL_PATH}/vendor/
|
||||
COPY . ${LARAVEL_PATH}
|
||||
COPY --from=frontend /app/public/js/ ${LARAVEL_PATH}/public/js/
|
||||
COPY --from=frontend /app/public/css/ ${LARAVEL_PATH}/public/css/
|
||||
COPY --from=frontend /app/public/mix-manifest.json ${LARAVEL_PATH}/public/mix-manifest.json
|
||||
|
||||
RUN set -x ; cd ${LARAVEL_PATH} \
|
||||
&& mkdir -p storage \
|
||||
&& mkdir -p storage/framework/cache \
|
||||
&& mkdir -p storage/framework/sessions \
|
||||
&& mkdir -p storage/framework/testing \
|
||||
&& mkdir -p storage/framework/views \
|
||||
&& mkdir -p storage/logs \
|
||||
&& chmod -R 777 storage \
|
||||
&& php artisan package:discover
|
||||
|
||||
FROM nginx:alpine as nginx
|
||||
|
||||
ARG LARAVEL_PATH=/app/laravel
|
||||
|
||||
COPY laravel.conf /etc/nginx/conf.d/
|
||||
COPY --from=laravel ${LARAVEL_PATH}/public ${LARAVEL_PATH}/public
|
||||
```
|
||||
85
04_image/other.md
Normal file
85
04_image/other.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# 其它制作镜像的方式
|
||||
|
||||
除了标准的使用 `Dockerfile` 生成镜像的方法外,由于各种特殊需求和历史原因,还提供了一些其它方法用以生成镜像。
|
||||
|
||||
## 从 rootfs 压缩包导入
|
||||
|
||||
格式:`docker import [选项] <文件>|<URL>|- [<仓库名>[:<标签>]]`
|
||||
|
||||
压缩包可以是本地文件、远程 Web 文件,甚至是从标准输入中得到。压缩包将会在镜像 `/` 目录展开,并直接作为镜像第一层提交。
|
||||
|
||||
比如我们想要创建一个 [OpenVZ](https://openvz.org) 的 Ubuntu 16.04 [模板](https://wiki.openvz.org/Download/template/precreated)的镜像:
|
||||
|
||||
```bash
|
||||
$ docker import \
|
||||
http://download.openvz.org/template/precreated/ubuntu-16.04-x86_64.tar.gz \
|
||||
openvz/ubuntu:16.04
|
||||
|
||||
Downloading from http://download.openvz.org/template/precreated/ubuntu-16.04-x86_64.tar.gz
|
||||
sha256:412b8fc3e3f786dca0197834a698932b9c51b69bd8cf49e100c35d38c9879213
|
||||
```
|
||||
|
||||
这条命令自动下载了 `ubuntu-16.04-x86_64.tar.gz` 文件,并且作为根文件系统展开导入,并保存为镜像 `openvz/ubuntu:16.04`。
|
||||
|
||||
导入成功后,我们可以用 `docker image ls` 看到这个导入的镜像:
|
||||
|
||||
```bash
|
||||
$ docker image ls openvz/ubuntu
|
||||
REPOSITORY TAG IMAGE ID CREATED SIZE
|
||||
openvz/ubuntu 16.04 412b8fc3e3f7 55 seconds ago 505MB
|
||||
```
|
||||
|
||||
如果我们查看其历史的话,会看到描述中有导入的文件链接:
|
||||
|
||||
```bash
|
||||
$ docker history openvz/ubuntu:16.04
|
||||
IMAGE CREATED CREATED BY SIZE COMMENT
|
||||
f477a6e18e98 About a minute ago 214.9 MB Imported from http://download.openvz.org/template/precreated/ubuntu-16.04-x86_64.tar.gz
|
||||
```
|
||||
|
||||
## Docker 镜像的导入和导出 `docker save` 和 `docker load`
|
||||
|
||||
Docker 还提供了 `docker save` 和 `docker load` 命令,用以将镜像保存为一个文件,然后传输到另一个位置上,再加载进来。这是在没有 Docker Registry 时的做法,现在已经不推荐,镜像迁移应该直接使用 Docker Registry,无论是直接使用 Docker Hub 还是使用内网私有 Registry 都可以。
|
||||
|
||||
### 保存镜像
|
||||
|
||||
使用 `docker save` 命令可以将镜像保存为归档文件。
|
||||
|
||||
比如我们希望保存这个 `alpine` 镜像。
|
||||
|
||||
```bash
|
||||
$ docker image ls alpine
|
||||
REPOSITORY TAG IMAGE ID CREATED SIZE
|
||||
alpine latest baa5d63471ea 5 weeks ago 4.803 MB
|
||||
```
|
||||
|
||||
保存镜像的命令为:
|
||||
|
||||
```bash
|
||||
$ docker save alpine -o filename
|
||||
$ file filename
|
||||
filename: POSIX tar archive
|
||||
```
|
||||
|
||||
这里的 filename 可以为任意名称甚至任意后缀名,但文件的本质都是归档文件
|
||||
|
||||
**注意:如果同名则会覆盖(没有警告)**
|
||||
|
||||
若使用 `gzip` 压缩:
|
||||
|
||||
```bash
|
||||
$ docker save alpine | gzip > alpine-latest.tar.gz
|
||||
```
|
||||
|
||||
然后我们将 `alpine-latest.tar.gz` 文件复制到了到了另一个机器上,可以用下面这个命令加载镜像:
|
||||
|
||||
```bash
|
||||
$ docker load -i alpine-latest.tar.gz
|
||||
Loaded image: alpine:latest
|
||||
```
|
||||
|
||||
如果我们结合这两个命令以及 `ssh` 甚至 `pv` 的话,利用 Linux 强大的管道,我们可以写一个命令完成从一个机器将镜像迁移到另一个机器,并且带进度条的功能:
|
||||
|
||||
```bash
|
||||
docker save <镜像名> | bzip2 | pv | ssh <用户名>@<主机名> 'cat | docker load'
|
||||
```
|
||||
232
04_image/pull.md
Normal file
232
04_image/pull.md
Normal file
@@ -0,0 +1,232 @@
|
||||
# 获取镜像
|
||||
|
||||
## docker pull 命令
|
||||
|
||||
从镜像仓库获取镜像的命令是 `docker pull`:
|
||||
|
||||
```bash
|
||||
docker pull [选项] [Registry地址/]仓库名[:标签]
|
||||
```
|
||||
|
||||
### 镜像名称格式
|
||||
|
||||
```
|
||||
docker.io / library / ubuntu : 24.04
|
||||
────┬──── ───┬─── ──┬─── ──┬──
|
||||
│ │ │ │
|
||||
Registry地址 用户名 仓库名 标签
|
||||
(可省略) (可省略)
|
||||
```
|
||||
|
||||
| 组成部分 | 说明 | 默认值 |
|
||||
|---------|------|--------|
|
||||
| Registry 地址 | 镜像仓库地址 | `docker.io`(Docker Hub) |
|
||||
| 用户名 | 镜像所属用户/组织 | `library`(官方镜像) |
|
||||
| 仓库名 | 镜像名称 | 必须指定 |
|
||||
| 标签 | 版本标识 | `latest` |
|
||||
|
||||
### 示例
|
||||
|
||||
```bash
|
||||
# 完整格式
|
||||
$ docker pull docker.io/library/ubuntu:24.04
|
||||
|
||||
# 省略 Registry(默认 Docker Hub)
|
||||
$ docker pull library/ubuntu:24.04
|
||||
|
||||
# 省略 library(官方镜像)
|
||||
$ docker pull ubuntu:24.04
|
||||
|
||||
# 省略标签(默认 latest)
|
||||
$ docker pull ubuntu
|
||||
|
||||
# 拉取第三方镜像
|
||||
$ docker pull bitnami/redis:latest
|
||||
|
||||
# 从其他 Registry 拉取
|
||||
$ docker pull ghcr.io/username/myapp:v1.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 下载过程解析
|
||||
|
||||
```bash
|
||||
$ docker pull ubuntu:24.04
|
||||
24.04: Pulling from library/ubuntu
|
||||
92dc2a97ff99: Pull complete
|
||||
be13a9d27eb8: Pull complete
|
||||
c8299583700a: Pull complete
|
||||
Digest: sha256:4bc3ae6596938cb0d9e5ac51a1152ec9dcac2a1c50829c74abd9c4361e321b26
|
||||
Status: Downloaded newer image for ubuntu:24.04
|
||||
docker.io/library/ubuntu:24.04
|
||||
```
|
||||
|
||||
### 输出解读
|
||||
|
||||
| 输出内容 | 说明 |
|
||||
|---------|------|
|
||||
| `Pulling from library/ubuntu` | 正在从官方 ubuntu 仓库拉取 |
|
||||
| `92dc2a97ff99: Pull complete` | 各层的下载状态(显示层 ID 前 12 位) |
|
||||
| `Digest: sha256:...` | 镜像内容的唯一摘要 |
|
||||
| `docker.io/library/ubuntu:24.04` | 镜像的完整名称 |
|
||||
|
||||
### 分层下载
|
||||
|
||||
从输出可以看到,镜像是**分层下载**的:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ubuntu:24.04 镜像 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 第3层 c8299583700a ───────► 已存在,跳过下载 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 第2层 be13a9d27eb8 ───────► 下载中... 完成 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 第1层 92dc2a97ff99 ───────► 下载中... 完成 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
如果本地已有相同的层,Docker 会跳过下载,节省带宽和时间。
|
||||
|
||||
---
|
||||
|
||||
## 常用选项
|
||||
|
||||
| 选项 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `--all-tags, -a` | 拉取所有标签 | `docker pull -a ubuntu` |
|
||||
| `--platform` | 指定平台架构 | `docker pull --platform linux/arm64 nginx` |
|
||||
| `--quiet, -q` | 静默模式 | `docker pull -q nginx` |
|
||||
|
||||
### 指定平台
|
||||
|
||||
在 Apple Silicon Mac 上拉取 x86 镜像:
|
||||
|
||||
```bash
|
||||
$ docker pull --platform linux/amd64 nginx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 拉取后运行
|
||||
|
||||
拉取镜像后,可以基于它启动容器:
|
||||
|
||||
```bash
|
||||
# 拉取镜像
|
||||
$ docker pull ubuntu:24.04
|
||||
|
||||
# 运行容器
|
||||
$ docker run -it --rm ubuntu:24.04 bash
|
||||
root@e7009c6ce357:/# cat /etc/os-release
|
||||
PRETTY_NAME="Ubuntu 24.04 LTS"
|
||||
...
|
||||
root@e7009c6ce357:/# exit
|
||||
```
|
||||
|
||||
**参数说明**:
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `-it` | 交互式终端模式 |
|
||||
| `--rm` | 退出后自动删除容器 |
|
||||
| `bash` | 启动命令 |
|
||||
|
||||
> 💡 `docker run` 在需要时会自动 `pull` 镜像,因此通常不需要单独执行 `docker pull`。
|
||||
|
||||
---
|
||||
|
||||
## 镜像加速
|
||||
|
||||
从 Docker Hub 下载可能较慢。可以配置镜像加速器:
|
||||
|
||||
```json
|
||||
// /etc/docker/daemon.json (Linux)
|
||||
// ~/.docker/daemon.json (Docker Desktop)
|
||||
{
|
||||
"registry-mirrors": [
|
||||
"https://your-accelerator-url"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
配置后重启 Docker:
|
||||
|
||||
```bash
|
||||
$ sudo systemctl restart docker # Linux
|
||||
# 或在 Docker Desktop 中重启
|
||||
```
|
||||
|
||||
详见 [镜像加速器](../install/mirror.md) 章节。
|
||||
|
||||
---
|
||||
|
||||
## 验证镜像完整性
|
||||
|
||||
### 查看镜像摘要
|
||||
|
||||
```bash
|
||||
$ docker images --digests ubuntu
|
||||
REPOSITORY TAG DIGEST IMAGE ID
|
||||
ubuntu 24.04 sha256:4bc3ae6596938cb0d9e5ac51a1152ec9dcac2a1c50829c74abd9c4361e321b26 ca2b0f26964c
|
||||
```
|
||||
|
||||
### 使用摘要拉取
|
||||
|
||||
用摘要拉取可确保获取完全相同的镜像:
|
||||
|
||||
```bash
|
||||
$ docker pull ubuntu@sha256:4bc3ae6596938cb0d9e5ac51a1152ec9dcac2a1c50829c74abd9c4361e321b26
|
||||
```
|
||||
|
||||
> 笔者建议:生产环境使用摘要而非标签,因为标签可能被覆盖,摘要则是不可变的。
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 下载速度很慢
|
||||
|
||||
1. 配置镜像加速器
|
||||
2. 检查网络连接
|
||||
3. 尝试拉取更小的镜像版本(如 `alpine` 变体)
|
||||
|
||||
### Q: 提示镜像不存在
|
||||
|
||||
```bash
|
||||
Error: pull access denied, repository does not exist
|
||||
```
|
||||
|
||||
可能原因:
|
||||
- 镜像名拼写错误
|
||||
- 私有镜像未登录(需要 `docker login`)
|
||||
- 镜像确实不存在
|
||||
|
||||
### Q: 磁盘空间不足
|
||||
|
||||
```bash
|
||||
# 清理未使用的镜像
|
||||
$ docker image prune
|
||||
|
||||
# 清理所有未使用资源
|
||||
$ docker system prune
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 本章小结
|
||||
|
||||
| 操作 | 命令 |
|
||||
|------|------|
|
||||
| 拉取镜像 | `docker pull 镜像名:标签` |
|
||||
| 拉取所有标签 | `docker pull -a 镜像名` |
|
||||
| 指定平台 | `docker pull --platform linux/amd64 镜像名` |
|
||||
| 用摘要拉取 | `docker pull 镜像名@sha256:...` |
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- [列出镜像](list.md):查看本地镜像
|
||||
- [删除镜像](rm.md):清理本地镜像
|
||||
- [镜像加速器](../install/mirror.md):加速镜像下载
|
||||
- [Docker Hub](../repository/dockerhub.md):官方镜像仓库
|
||||
255
04_image/rm.md
Normal file
255
04_image/rm.md
Normal file
@@ -0,0 +1,255 @@
|
||||
# 删除本地镜像
|
||||
|
||||
## 基本用法
|
||||
|
||||
使用 `docker image rm` 删除本地镜像:
|
||||
|
||||
```bash
|
||||
$ docker image rm [选项] <镜像1> [<镜像2> ...]
|
||||
```
|
||||
|
||||
> 💡 `docker rmi` 是 `docker image rm` 的简写,两者等效。
|
||||
|
||||
---
|
||||
|
||||
## 镜像标识方式
|
||||
|
||||
删除镜像时,可以使用多种方式指定镜像:
|
||||
|
||||
| 方式 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| **短 ID** | ID 的前几位(通常 3-4 位) | `docker rmi 501` |
|
||||
| **长 ID** | 完整的镜像 ID | `docker rmi 501ad78535f0...` |
|
||||
| **镜像名:标签** | 仓库名和标签 | `docker rmi redis:alpine` |
|
||||
| **镜像摘要** | 精确的内容摘要 | `docker rmi nginx@sha256:...` |
|
||||
|
||||
### 使用短 ID 删除
|
||||
|
||||
```bash
|
||||
$ docker image ls
|
||||
REPOSITORY TAG IMAGE ID SIZE
|
||||
redis alpine 501ad78535f0 30MB
|
||||
nginx latest e43d811ce2f4 142MB
|
||||
|
||||
# 只需输入足够区分的前几位
|
||||
$ docker rmi 501
|
||||
Untagged: redis:alpine
|
||||
Deleted: sha256:501ad78535f0...
|
||||
```
|
||||
|
||||
### 使用镜像名删除
|
||||
|
||||
```bash
|
||||
$ docker rmi redis:alpine
|
||||
Untagged: redis:alpine
|
||||
Deleted: sha256:501ad78535f0...
|
||||
```
|
||||
|
||||
### 使用摘要删除
|
||||
|
||||
摘要删除最精确,适用于 CI/CD 场景:
|
||||
|
||||
```bash
|
||||
# 查看镜像摘要
|
||||
$ docker images --digests
|
||||
REPOSITORY TAG DIGEST IMAGE ID
|
||||
nginx latest sha256:b4f0e0bdeb5... e43d811ce2f4
|
||||
|
||||
# 使用摘要删除
|
||||
$ docker rmi nginx@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 理解输出信息
|
||||
|
||||
删除镜像时会看到两类信息:**Untagged** 和 **Deleted**
|
||||
|
||||
```bash
|
||||
$ docker rmi redis:alpine
|
||||
Untagged: redis:alpine
|
||||
Untagged: redis@sha256:f1ed3708f538b537eb9c2a7dd50dc90a706f7debd7e1196c9264edeea521a86d
|
||||
Deleted: sha256:501ad78535f015d88872e13fa87a828425117e3d28075d0c117932b05bf189b7
|
||||
Deleted: sha256:96167737e29ca8e9d74982ef2a0dda76ed7b430da55e321c071f0dbff8c2899b
|
||||
Deleted: sha256:32770d1dcf835f192cafd6b9263b7b597a1778a403a109e2cc2ee866f74adf23
|
||||
```
|
||||
|
||||
### Untagged vs Deleted
|
||||
|
||||
| 操作 | 含义 |
|
||||
|------|------|
|
||||
| **Untagged** | 移除镜像的标签 |
|
||||
| **Deleted** | 删除镜像的存储层 |
|
||||
|
||||
### 删除流程
|
||||
|
||||
```
|
||||
docker rmi redis:alpine
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ 1. Untag:移除 redis:alpine 标签 │
|
||||
│ ↓ │
|
||||
│ 2. 检查是否还有其他标签指向这个镜像 │
|
||||
│ ├── 有 → 只 Untag,不删除 │
|
||||
│ └── 无 → │
|
||||
│ ↓ │
|
||||
│ 3. 检查是否有容器依赖 │
|
||||
│ ├── 有 → 报错,无法删除 │
|
||||
│ └── 无 → │
|
||||
│ ↓ │
|
||||
│ 4. 从上到下逐层删除,检查每层是否被其他镜像使用 │
|
||||
│ ├── 被使用 → 保留 │
|
||||
│ └── 未使用 → Deleted │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 批量删除
|
||||
|
||||
### 删除所有虚悬镜像
|
||||
|
||||
虚悬镜像(dangling):没有标签的镜像,通常是旧版本被新版本覆盖后产生的
|
||||
|
||||
```bash
|
||||
# 查看虚悬镜像
|
||||
$ docker images -f dangling=true
|
||||
|
||||
# 删除虚悬镜像
|
||||
$ docker image prune
|
||||
|
||||
# 不提示确认
|
||||
$ docker image prune -f
|
||||
```
|
||||
|
||||
### 删除所有未使用的镜像
|
||||
|
||||
```bash
|
||||
# 删除所有没有被容器使用的镜像
|
||||
$ docker image prune -a
|
||||
|
||||
# 保留最近 24 小时的
|
||||
$ docker image prune -a --filter "until=24h"
|
||||
```
|
||||
|
||||
### 按条件删除
|
||||
|
||||
```bash
|
||||
# 删除所有 redis 镜像
|
||||
$ docker rmi $(docker images -q redis)
|
||||
|
||||
# 删除 mongo:8.0 之前的所有镜像
|
||||
$ docker rmi $(docker images -q -f before=mongo:8.0)
|
||||
|
||||
# 删除某个时间之前的镜像
|
||||
$ docker image prune -a --filter "until=168h" # 7天前
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 删除失败的常见原因
|
||||
|
||||
### 原因一:有容器依赖
|
||||
|
||||
```bash
|
||||
$ docker rmi nginx
|
||||
Error: conflict: unable to remove repository reference "nginx"
|
||||
(must force) - container abc123 is using its referenced image
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
|
||||
```bash
|
||||
# 方案1:先删除依赖的容器
|
||||
$ docker rm abc123
|
||||
$ docker rmi nginx
|
||||
|
||||
# 方案2:强制删除镜像(容器仍可运行,但无法再创建新容器)
|
||||
$ docker rmi -f nginx
|
||||
```
|
||||
|
||||
### 原因二:多个标签指向同一镜像
|
||||
|
||||
```bash
|
||||
$ docker images
|
||||
REPOSITORY TAG IMAGE ID
|
||||
ubuntu 24.04 ca2b0f26964c
|
||||
ubuntu latest ca2b0f26964c # 同一个镜像
|
||||
|
||||
$ docker rmi ubuntu:24.04
|
||||
Untagged: ubuntu:24.04
|
||||
# 只是移除标签,镜像仍存在(因为还有 ubuntu:latest 指向它)
|
||||
```
|
||||
|
||||
### 原因三:被其他镜像依赖(中间层)
|
||||
|
||||
```bash
|
||||
$ docker rmi some_base_image
|
||||
Error: image has dependent child images
|
||||
```
|
||||
|
||||
中间层镜像被其他镜像依赖,无法删除。需要先删除依赖它的镜像。
|
||||
|
||||
---
|
||||
|
||||
## 常用过滤条件
|
||||
|
||||
| 过滤条件 | 说明 | 示例 |
|
||||
|---------|------|------|
|
||||
| `dangling=true` | 虚悬镜像 | `-f dangling=true` |
|
||||
| `before=镜像` | 在某镜像之前 | `-f before=mongo:3.2` |
|
||||
| `since=镜像` | 在某镜像之后 | `-f since=mongo:3.2` |
|
||||
| `label=key=value` | 按标签过滤 | `-f label=version=1.0` |
|
||||
| `reference=pattern` | 按名称模式 | `-f reference='*:latest'` |
|
||||
|
||||
---
|
||||
|
||||
## 清理策略
|
||||
|
||||
### 开发环境
|
||||
|
||||
```bash
|
||||
# 定期清理虚悬镜像
|
||||
$ docker image prune -f
|
||||
|
||||
# 一键清理所有未使用资源
|
||||
$ docker system prune -a
|
||||
```
|
||||
|
||||
### CI/CD 环境
|
||||
|
||||
```bash
|
||||
# 只保留最近使用的镜像
|
||||
$ docker image prune -a --filter "until=72h" -f
|
||||
```
|
||||
|
||||
### 查看空间占用
|
||||
|
||||
```bash
|
||||
$ docker system df
|
||||
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
|
||||
Images 15 3 2.5GB 1.8GB (72%)
|
||||
Containers 5 2 100MB 80MB (80%)
|
||||
Local Volumes 8 2 500MB 400MB (80%)
|
||||
Build Cache 0 0 0B 0B
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 本章小结
|
||||
|
||||
| 操作 | 命令 |
|
||||
|------|------|
|
||||
| 删除指定镜像 | `docker rmi 镜像名:标签` |
|
||||
| 强制删除 | `docker rmi -f 镜像名` |
|
||||
| 删除虚悬镜像 | `docker image prune` |
|
||||
| 删除未使用镜像 | `docker image prune -a` |
|
||||
| 批量删除 | `docker rmi $(docker images -q -f ...)` |
|
||||
| 查看空间占用 | `docker system df` |
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- [列出镜像](list.md):查看和过滤镜像
|
||||
- [删除容器](../05_container/rm.md):清理容器
|
||||
- [数据卷](../07_data_network/data/volume.md):清理数据卷
|
||||
Reference in New Issue
Block a user