🐳 获取镜像
在 docker 世界中,镜像是构建容器的基石。🔍 你可以想象镜像就像一个模板,我们可以用它来创建多个相似的容器。
- docker hub 宝库**:docker hub 上有大量的高质量的镜像,等待着你的探索和使用。
- 拉取命令:使用
docker pull
从 docker 镜像仓库获取你所需的镜像。
例如:$ docker pull [选项] [docker registry 地址[:端口号]/]仓库名[:标签]
$ docker pull ubuntu:18.04
🔧 运行镜像
有了镜像后,就可以魔法一般地运行它,创建出一个真正的容器。
-
命令:使用
docker run
命令来运行镜像并创建容器。$ docker run -it --rm ubuntu:18.04 bash
-
参数解释:
-it
:这是两个参数的组合。-i
代表交互式操作,-t
为我们提供一个终端。--rm
:完成后删除容器,保持清洁。ubuntu:18.04
:使用此镜像。bash
:我们想要运行的命令,这里是启动一个 bash shell。
-
在容器内部:一旦你进入容器,你就可以执行你需要的任何命令。例如,查看系统版本:
root@e7009c6ce357:/# cat /etc/os-release
-
退出容器
-
当你完成了容器中的工作,你可能会想退出并返回到主机。
- 退出命令:简单地在容器的终端中输入
exit
。 - 记住:如果你在
docker run
命令中使用了--rm
参数,容器将在你退出时被自动删除,保持你的工作环境整洁。
- 退出命令:简单地在容器的终端中输入
-
🖼️ 列出镜像
在你的 docker 环境中,可能存在多个镜像。以下是如何列出它们:
-
基本命令:使用
docker image ls
来查看所有顶层镜像。$ docker image ls
这将显示类似以下的输出:
repository tag image id created size redis latest 5f515359c7f8 5 days ago 183 mb nginx latest 05a60462f8ba 5 days ago 181 mb mongo 3.2 fe9198c04d62 5 days ago 342 mb ...
-
虚悬镜像 🚫:它们没有标签和仓库名,因此可能不再需要。使用以下命令查看:
$ docker image ls -f dangling=true
要删除这些镜像,可以使用:
$ docker image prune
-
中间层镜像 🍰:这些是 docker 使用来加速构建的镜像。要看所有的,包括中间层镜像,使用:
$ docker image ls -a
-
列出部分镜像 🎯:有时你只想查看特定的镜像。例如,要查看所有的
ubuntu
镜像,可以使用:$ docker image ls ubuntu
-
自定义格式 📝:如果你想以特定的格式显示镜像列表,可以使用
--format
参数。例如:$ docker image ls --format "{{.id}}: {{.repository}}"
📌 注意:docker 镜像是构建和运行容器的基石。理解和管理你的镜像库是 docker 使用中的一个关键部分。
🗑️ 删除本地镜像
当你的 docker 环境中的镜像积累过多,或者你想释放一些磁盘空间时,你可能会想删除一些不再需要的镜像。以下是如何进行操作:
-
基本命令:使用
docker image rm
来删除指定的镜像。$ docker image rm <镜像名称或id>
-
多种方式删除:可以使用镜像的完整 id、短 id、镜像名或摘要来指定要删除的镜像。
例如,删除 redis:alpine 镜像:
$ docker image rm redis:alpine
也可以使用短id:
$ docker image rm 501
更精确的是使用镜像摘要:
$ docker image rm node@sha256:<摘要>
-
注意删除的内容:删除镜像时,首先是取消该镜像的标签(untagged),然后真实删除镜像内容(deleted)。
-
容器的影响:如果一个容器基于某个镜像运行,那么该镜像在容器存在的情况下是不能被删除的。必须先删除容器,然后才能删除镜像。
-
结合
docker image ls
使用:要删除多个镜像,可以结合docker image ls -q
来一次删除多个镜像。例如,删除所有名为
redis
的镜像:$ docker image rm $(docker image ls -q redis)
删除在
mongo:3.2
之前的所有镜像:$ docker image rm $(docker image ls -q -f before=mongo:3.2)
📌 注意:删除操作是不可逆的。在删除任何镜像之前,确保你不再需要它,或者你有备份。
🎨 利用 commit
理解镜像构成
-
🚀 1. 运行一个 nginx 容器:
$ docker run --name webserver -d -p 80:80 nginx
启动基于
nginx
镜像的webserver
容器,并映射 80 端口。 -
🛠️ 2. 修改容器内的文件:
进入容器并更改内容:$ docker exec -it webserver bash root@container_id:/# echo '<h1>hello, docker!</h1>' > /usr/share/nginx/html/index.html
-
🔍 3. 查看文件更改:
使用docker diff
查看容器文件更改:$ docker diff webserver
-
📦 4. 提交修改:
使用docker commit
创建一个新镜像,基于原始nginx
镜像并添加我们在容器内的更改:$ docker commit \ --author "your name <your_email@example.com>" \ --message "修改了默认网页" \ webserver \ nginx:v2
-
📜 5. 查看镜像的历史记录:
用docker history
查看新镜像的历史记录:$ docker history nginx:v2
⚠️ 慎用 docker commit
:
- 使用
docker commit
会产生 “黑箱镜像”,维护和追溯困难。 - 镜像的分层存储意味着使用
docker commit
会使镜像臃肿。
📖 docker commit 命令解析**:
docker commit
允许从修改过的容器创建新镜像。
格式:
docker commit [选项] <容器id或容器名> [<仓库名>[:<标签>]]
[选项]
: 如--author
或--message
.<容器id或容器名>
: 要提交的容器。[<仓库名>[:<标签>]]
: 新镜像名称和标签。
示例:
想保存 webserver
容器更改为 nginx_modified:v2
镜像:
$ docker commit --author "your name <your_email@example.com>" --message "修改了默认网页" webserver nginx_modified:v2
✨ 推荐做法:
- 使用
dockerfile
描述和构建镜像,以确保透明性和可维护性。
🤖 使用 dockerfile
定制镜像
在之前的 docker commit
学习中,我们了解到镜像定制实际上是对每一层所添加的配置、文件的定制。而 dockerfile
正是为此设计的。
dockerfile 是一个包含用于组合镜像的指令的文本文档。每一条指令都会在镜像上添加一个新层,然后提交。
🚀 创建一个基于 nginx 的 dockerfile
- 在一个空目录中创建
dockerfile
:
$ mkdir mynginx
$ cd mynginx
$ touch dockerfile
dockerfile
内容:
from nginx
run echo '<h1>hello, docker!</h1>' > /usr/share/nginx/html/index.html
📚 dockerfile 指令
- from: 指定基础镜像。每个
dockerfile
都必须开始于此。 - run: 执行命令。
🎓 更深入地了解指令
-
from: 您可以使用任何公开的镜像作为基础镜像,或者从零开始构建。
from scratch
是一个特殊的基础镜像,表示一个空白镜像。
-
run: 运行命令行命令。有两种格式:
- shell 格式:
run <命令>
- exec 格式:
run ["可执行文件", "参数1", "参数2"]
- shell 格式:
🛠️ 构建镜像
使用 docker build
命令构建一个镜像。其格式为: docker build [选项] <上下文路径/url/->
。
例如:
$ docker build -t nginx:v3 .
🌐 镜像构建上下文
-
上下文是
docker build
命令所需的文件和目录。这些内容将被打包并发送到 docker 守护进程以构建镜像。 -
注意: 请确保不要包括不需要的文件到您的上下文中,因为这会使镜像变得臃肿。
🔍 其他构建方法
- 从 git 仓库直接构建:
$ docker build -t hello-world https://github.com/docker-library/hello-world.git#master:amd64/hello-world
- 从给定的 tar 压缩包构建:
$ docker build http://server/context.tar.gz
- 从标准输入中读取 dockerfile:
docker build - < dockerfile
或
cat dockerfile | docker build -
- 从标准输入中读取上下文压缩包:
$ docker build - < context.tar.gz
🔥 dockerfile 指令详解
指令 | 作用 | 使用场景 | 注意事项 |
---|---|---|---|
from | 设置基础镜像 | 创建新的镜像时 | 必须是 dockerfile 的第一条非注释指令 |
maintainer | 设置镜像的作者信息 | 提供联系方式和作者信息 | 已被 label 替代,但仍可用 |
run | 执行命令并创建新的镜像层 | 安装软件包或执行其他配置命令 | 应避免在一个 run 指令中运行多条命令,以减少镜像大小 |
cmd | 提供容器的默认执行命令 | 当容器启动时运行的默认命令 | 如果 dockerfile 中有多个 cmd 指令,只有最后一个会生效 |
entrypoint | 配置容器启动后执行的命令 | 当容器作为一个可执行程序 | 可以结合 cmd 指令使用 |
copy | 从上下文目录中复制文件或目录到容器 | 将本地文件复制到镜像中 | 注意文件和目录的权限设置 |
add | 从上下文目录复制文件,可以自动解压缩归档文件 | 当需要自动解压缩归档文件时 | 通常推荐使用 copy ,除非需要自动解压缩功能 |
env | 设置环境变量 | 设置持久化的环境变量 | 可以在 run , cmd , entrypoint 等指令中使用 |
arg | 设置构建参数 | 传递给构建运行时的变量 | 只在构建时生效,不会在容器运行时生效 |
volume | 创建挂载点并挂载文件系统 | 当容器需要保存数据或共享数据时 | 用于声明容器内的路径需要作为卷 |
expose | 声明容器运行时监听的端口 | 当容器运行服务时 | 只是声明,不会自动在宿主机上映射端口 |
workdir | 设置工作目录 | 改变后续指令的执行目录 | 如果指定的目录不存在,会自动创建 |
user | 指定运行用户 | 当容器的命令需要特定用户权限时 | 用户必须在之前的层中定义,通常通过 run 指令与 useradd 命令 |
healthcheck | 设置容器健康检查 | 判断容器的状态是否正常 | 检查命令的返回值为 0 表示健康,1 表示不健康 |
label | 为镜像添加元数据 | 添加额外的元数据,如版本、描述等 | 可用于存储如作者、版本、构建日期等信息的键值对 |
shell | 指定 run , cmd , entrypoint 的 shell | 改变默认的 /bin/sh -c | 注意替换的 shell 和其参数 |
onbuild | 为子镜像提供触发指令 | 当创建一个作为基础镜像使用的镜像时 | 在构建基础镜像时不会执行,但在基于该基础镜像构建子镜像时会执行 |
☁️ from 指定基础镜像
🖋️ 格式:
from <image>[:<tag>] [as <name>]
🚀 示例与详解:
from ubuntu:20.04 as build-stage
这里使用 ubuntu 20.04 作为基础镜像并为这个构建阶段命名为 “build-stage”。
📚 注意事项:
from
必须是 dockerfile 中的第一条非注释指令(除非在多阶段构建中使用了多个from
指令)。- 一个 dockerfile 可以包含多个
from
指令,用于多阶段构建。 - 指定的基础镜像必须是一个已存在或可以从公有/私有仓库拉取的镜像。
- 如果 dockerfile 只是基于一个已存在的镜像,而没有任何额外的定制化指令,那么这个
from
指令可以是 dockerfile 中的唯一指令。
☁️ run 执行命令
🖋️ 格式:
run <command> (shell 格式)
或
run ["executable", "param1", "param2"] (exec 格式)
🚀 示例与详解:
- shell 格式:
run apt-get update && apt-get install -y curl
这里,我们使用的是默认的 shell 格式,运行命令更新系统软件包并安装 curl。
- exec 格式:
run ["apt-get", "update"]
在这个例子中,我们使用 exec 格式执行相同的更新操作。
📚 注意事项:
run
指令会在新的层上执行指定的命令,并提交结果。这个提交的层会成为下一条指令的基础层。- 如果可能的话,尽量将多个相关的命令组合在一个
run
指令中,以减少创建的层数。 - 清理不必要的文件和数据,例如使用
apt-get install
后进行清理,以减少镜像大小。 - 要注意避免在一个
run
指令中执行多条无关的命令,因为这会使得缓存失效,增加构建时间。 - 当使用 exec 格式时,不能使用 shell 的特性,例如变量替换和通配符。
📁 copy 复制文件
🖋️ 格式:
-
常规格式:
copy [--chown=<user>:<group>] <源路径>... <目标路径>
-
json 数组格式:
copy [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]
🚀 示例与详解:
-
基本复制:
示例:
copy package.json /usr/src/app/
详解:
这个命令将当前上下文中的package.json
文件复制到镜像的/usr/src/app/
目录中。 -
使用通配符:
示例:
copy hom* /mydir/ copy hom?.txt /mydir/
详解:
第一个命令将所有以hom
开头的文件复制到/mydir/
目录。第二个命令将匹配hom
后面跟一个字符并以.txt
结尾的文件。 -
指定用户和组:
示例:
copy --chown=55:mygroup files* /mydir/
详解:
该命令将所有以files
开头的文件复制到/mydir/
并更改其所有者为用户 id55
和组mygroup
。 -
源路径是目录:
示例:
copy ./mydir/ /targetdir/
详解:
如果./mydir/
是一个目录,则 docker 会将该目录下的内容复制到/targetdir/
,而不是整个mydir
目录。 -
使用 json 数组格式指定路径中包含空格的文件:
示例:
copy ["my file.txt", "/path with spaces/"]
详解:
当文件或路径名中包含空格时,需要使用 json 数组格式来确保文件名被正确解析。
📚 注意事项:
-
copy
命令使用的所有文件和目录都应位于 docker 构建的上下文中。 -
使用
copy
命令时,所有文件的元数据,如权限和修改时间,都会被保留。 -
如果目标路径不存在,docker 会自动创建。
📁 add 更高级的复制文件
🖋️ 格式:
-
常规格式:
add [--chown=<user>:<group>] <源路径>... <目标路径>
-
json 数组格式:
add [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]
🚀 示例与详解:
-
从url添加文件:
示例:
add http://example.com/big.tar.xz /usr/src/big/
详解:
这个命令尝试下载http://example.com/big.tar.xz
并解压到/usr/src/big/
。但如上所述,这种方式并不推荐,因为它会使得 dockerfile 的透明性和可重复性降低。 -
自动解压缩 tar 文件:
示例:
add local-big.tar.xz /usr/src/big/
详解:
如果local-big.tar.xz
是一个本地的tar文件,docker将会自动解压这个tar文件到镜像的/usr/src/big/
。 -
指定用户和组:
示例:
add --chown=55:mygroup files* /mydir/
详解:
该命令将所有以files
开头的文件复制到/mydir/
并更改其所有者为用户 id55
和组mygroup
。
📚 注意事项:
-
与
copy
类似,add
命令使用的所有文件和目录都应位于 docker 构建的上下文中。 -
除非有明确需求,否则推荐使用
copy
指令,因为它的语义更明确。 -
如果你只是希望添加一个压缩文件而不想解压,那么你应该使用
copy
。 -
使用
add
指令时,文件的元数据,如权限和修改时间,都会被保留。 -
如果目标路径不存在,docker会自动创建它。
🖥️ cmd容器启动命令
🖋️ 格式:
-
shell 格式:
cmd <命令>
-
exec 格式:
cmd ["可执行文件", "参数1", "参数2"...]
-
参数列表格式 (与 entrypoint 配合使用):
cmd ["参数1", "参数2"...]
🚀 示例与详解:
-
默认启动命令:
示例:
cmd /bin/bash
详解:
当容器启动时,默认执行/bin/bash
。如果用户没有提供命令,例如docker run -it ubuntu
,则容器内部会运行/bin/bash
。但用户可以覆盖默认命令,例如:docker run -it ubuntu cat /etc/os-release
。 -
使用 exec 格式:
示例:
cmd ["echo", "$home"]
详解:
推荐使用此格式,因为它更明确,不会受到 shell 的影响。但在此示例中,环境变量$home
不会被解析,因为它不是在 shell 中执行的。 -
错误的后台运行方式:
示例:
cmd service nginx start
详解:
这是一个常见的错误。此命令会使 nginx 作为后台守护进程启动,而容器的主进程会很快结束,导致容器也跟着结束。 -
正确的前台运行方式:
示例:
cmd ["nginx", "-g", "daemon off;"]
详解:
此命令会使 nginx 以前台模式运行,这是容器的正确操作方式。容器的主进程是 nginx,当 nginx 运行时,容器也会运行。
📚 注意事项:
-
cmd
指令是用于指定容器启动时的默认命令及参数的。 -
在一个 dockerfile 中,可以有多个
cmd
指令,但只有最后一个cmd
会生效。 -
cmd
和entrypoint
的组合非常有用。entrypoint
定义了容器启动时运行的命令,而cmd
则提供了默认的参数。 -
容器的应用应该始终以前台模式运行。后台模式是传统虚拟机或物理服务器的概念,在容器技术中不适用。
-
如果容器的主进程结束,容器也会结束。
🖥️ entrypoint 入口点
🖋️ 格式:
-
shell 格式:
entrypoint <命令>
-
exec 格式:
entrypoint ["可执行文件", "参数1", "参数2"...]
🚀 示例与详解:
-
基础使用:
示例:
entrypoint ["echo", "hello"] cmd ["world"]
详解:
当容器启动时,默认执行echo hello world
。但如果用户提供了命令参数,例如docker run <image_name> docker
,则会执行echo hello docker
。 -
结合 cmd 使用:
示例:
entrypoint ["curl", "-s", "http://myip.ipip.net"]
详解:
使用此镜像,你可以直接查询公网 ip。如果需要额外参数(例如-i
来显示 http 头),你可以执行docker run <image_name> -i
。 -
预处理脚本:
示例:
from alpine:3.4 run addgroup -s redis && adduser -s -g redis redis entrypoint ["docker-entrypoint.sh"] cmd [ "redis-server" ]
详解:
这是 redis 官方镜像的一个简化示例。在这里,创建了一个 redis 用户,然后使用docker-entrypoint.sh
脚本作为 entrypoint。这个脚本检查 cmd 的内容并执行适当的操作。如果 cmd 是redis-server
,脚本会切换到 redis 用户并启动服务器。否则,它会以 root 用户身份执行命令。
📚 注意事项:
-
entrypoint
指令用于指定容器启动时的默认命令。 -
entrypoint
和cmd
可以结合使用。当这两者都存在时,cmd
的内容会作为参数传递给entrypoint
。 -
如果需要覆盖
entrypoint
,可以使用docker run
的--entrypoint
参数。 -
entrypoint
通常用于指定一个固定的程序(及其参数),而cmd
用于指定默认的参数。 -
一些常见的用例包括使用
entrypoint
为容器提供一个主应用程序,并允许用户使用cmd
来提供应用程序的默认参数。 -
使用
entrypoint
的一个常见场景是容器化的应用程序需要执行一些启动前的初始化任务。这可以通过将初始化脚本设置为entrypoint
,然后在脚本的末尾执行主应用程序来实现。
🌍 env 设置环境变量
🖋️ 格式:
-
单一变量设置:
env <key> <value>
-
多变量设置:
env <key1>=<value1> <key2>=<value2>...
🚀 示例与详解:
-
基础使用:
示例:
env my_version 1.0
详解:
通过这种方式,我们设置了一个名为my_version
的环境变量,其值为1.0
。此后,该变量可在 dockerfile 的后续指令或容器内部使用。 -
多变量设置:
示例:
env name="john doe" age=30 country="united states"
详解:
在一个env
指令中,我们可以同时设置多个环境变量。这里我们设置了name
,age
和country
三个环境变量。 -
环境变量使用:
示例:
env app_path /usr/local/app workdir $app_path
详解:
一旦设置了环境变量,就可以在后续的 dockerfile 指令中使用它们。在这个示例中,我们首先设置了app_path
变量,然后在workdir
指令中使用它,这样workdir
就会设置为/usr/local/app
。
📚 注意事项:
-
env
指令不仅可以在 dockerfile 中设置环境变量,这些变量还会被复制到最终的镜像中,因此在容器运行时也可以使用这些环境变量。 -
使用环境变量可以提高 dockerfile 的可读性和可维护性。例如,如果你的应用有一个版本号需要在多个地方使用,你可以将版本号设置为一个环境变量,这样当需要更新版本时,只需修改
env
指令即可。 -
env
指令在构建过程中创建的每个环境变量都会增加镜像层的数量,因此,为了减少镜像层的数量,建议在一个env
指令中设置多个环境变量。 -
当你运行一个容器时,也可以通过
-e
参数来设置新的环境变量或覆盖已有的环境变量。
🌍 arg 构建参数
🖋️ 格式:
arg <参数名>[=<默认值>]
🚀 示例与详解:
-
基础使用:
示例:
arg my_version=1.0
详解:
这里定义了一个名为my_version
的构建参数,并为其设置了默认值1.0
。在接下来的构建过程中,你可以引用这个变量。 -
使用构建参数:
示例:
arg base_image_version=latest from my-base-image:${base_image_version}
详解:
通过这种方式,你可以在构建时通过--build-arg base_image_version=2.0
来选择不同版本的基础镜像进行构建。 -
多阶段构建中的构建参数:
示例:
arg docker_username=library from ${docker_username}/alpine as stage1 run echo "this is stage 1" arg docker_username=library from ${docker_username}/alpine as stage2 run echo "this is stage 2"
详解:
在多阶段构建中,每个阶段都可以有其自己的构建参数。这些参数必须在每个阶段中重新声明,以便在该阶段中使用。
📚 注意事项:
-
arg
设置的变量仅在构建期间存在,并不会保留在最终的镜像中。这与env
指令不同,后者设置的环境变量会被保留在最终的镜像中。 -
虽然
arg
设置的构建参数在最终的镜像中不可见,但是在构建过程中(例如docker history
的输出中)可能仍然可以看到这些值,因此不应使用arg
来存储敏感信息。 -
如果你在
from
指令之前定义了一个构建参数,那么这个参数只能在from
指令中使用。要在后续指令中使用它,你需要在每个构建阶段重新声明它。 -
使用构建参数可以提高 dockerfile 的灵活性,使你能够用同一个 dockerfile 构建不同配置或版本的镜像。
📦 volume 定义匿名卷
🖋️ 格式:
volume ["<路径1>", "<路径2>"...]
或者
volume <路径>
🚀 示例与详解:
-
基础使用:
示例:
volume /app/data
详解:
这里定义了一个名为/app/data
的匿名卷。在容器运行期间,这个目录的内容将存储在 docker 主机上的某个位置,而不是在容器的文件系统中。这有助于保持容器的无状态性。 -
多路径匿名卷:
示例:
volume ["/app/data", "/app/config"]
详解:
这里定义了两个匿名卷,分别为/app/data
和/app/config
。两者都会在容器运行时自动挂载为匿名卷。
📚 注意事项:
-
volume
指令创建的卷是匿名的,这意味着它们没有一个易于识别的名称。但是,docker 会为它们分配一个随机字符串作为名称。 -
使用
volume
为容器数据提供了一个永久存储解决方案,因为匿名卷存储在 docker 主机上,与容器的生命周期是独立的。这意味着,即使容器被删除,卷中的数据仍然存在。 -
尽管
volume
提供了一个方便的方法来定义匿名卷,但在实践中,使用命名卷(如上面的mydata
示例)通常更受欢迎,因为它们更容易管理和识别。 -
如果你在 dockerfile 中使用了
volume
指令,但在运行容器时又使用-v
或--volume
选项来挂载一个卷,那么运行时的设置会覆盖 dockerfile 中的设置。
🚪 expose 暴露端口
🖋️ 格式:
expose <端口1> [<端口2>...]
🚀 示例与详解:
-
基础使用:
示例:
expose 80
详解:
这里声明了容器意图使用80端口提供服务。但这仅仅是一个声明,并不意味着当容器运行时,主机会自动映射到这个端口。 -
声明多个端口:
示例:
expose 80 443
详解:
这里声明了容器打算使用80端口和443端口。这可能意味着容器可能既提供http服务也提供https服务。
📚 注意事项:
-
expose
只是一个元数据指令,它告诉使用镜像的人这个容器内部的应用监听的端口。但它并不会使容器的端口在主机上可访问。 -
要使容器的端口在主机上可访问,你需要在
docker run
命令中使用-p
参数来发布并映射一个或多个端口,或者使用-p
参数来发布所有expose
指令中声明的端口。 -
即使没有在 dockerfile 中使用
expose
,你仍然可以在docker run
命令中使用-p
参数来发布容器的任何端口。 -
为了容器的可读性和可维护性,建议在 dockerfile 中为应用程序使用的每个端口都使用一个
expose
指令。
📁 workdir 指定工作目录
🖋️ 格式:
workdir <工作目录路径>
🚀 示例与详解:
-
基础使用:
示例:
workdir /app
详解:
此指令会将当前工作目录设置为容器中的/app
。如果/app
目录不存在,docker 将创建它。所有后续指令(如run
,cmd
,entrypoint
等)都将在此目录中执行。 -
相对路径:
示例:
workdir /a workdir b run pwd
详解:
pwd
的输出将是/a/b
。这是因为第二个workdir
指令使用了相对路径,所以它是相对于前一个workdir
指令设置的路径。 -
链式目录:
示例:
workdir /a/b/c run pwd
详解:
pwd
的输出将是/a/b/c
。此指令将工作目录设置为/a/b/c
,并确保这个路径存在。
📚 注意事项:
-
workdir
不仅仅是为了指定run
,cmd
和entrypoint
的工作目录。它也为后续的指令(如copy
和add
)提供了上下文。 -
如果你在 dockerfile 中多次更改工作目录,每个
workdir
会为镜像添加新的层。尽管这些层很小,但最好减少不必要的层。 -
通常建议在 dockerfile 的开始部分设置一个工作目录,并在此目录中执行所有后续操作,以保持清晰和组织良好。
-
不推荐使用
run cd /some/path
,因为这样的改变只在那个特定的run
指令中生效,对后续的指令没有影响。
🧑 user 指定当前用户
🖋️ 格式:
user <用户名>[:<用户组>]
🚀 示例与详解:
-
基础使用:
示例:
user daemon
详解:
此指令会更改所有后续指令的运行用户为daemon
。这意味着所有后续的run
,cmd
, 和entrypoint
指令都将以daemon
用户的身份执行。 -
指定用户和用户组:
示例:
user redis:redis
详解:
在此示例中,所有后续指令都将以redis
用户及其对应的redis
用户组的身份执行。
📚 注意事项:
-
user
指令定义的用户必须在之前的层中预先创建,否则构建将失败。例如,要使用名为redis
的用户,你可能需要先执行如下的run
指令来创建它:run useradd -ms /bin/bash redis
-
在设计 dockerfile 时,应考虑在其中运行的应用程序或服务不以
root
用户身份运行,这是为了增加容器的安全性。 -
如果需要临时切换回
root
用户来执行某些命令(例如,安装软件包),你可以再次使用user
指令来切换回root
,然后再次切换到另一个用户。 -
如果在脚本中需要切换用户,使用
gosu
是一个好选择,因为它的行为比su
或sudo
更容易预测,并且不需要额外的配置。
🏥 healthcheck 健康检查
🖋️ 格式:
- 定义检查命令:
healthcheck [选项] cmd <命令>
- 取消基础镜像的健康检查:
healthcheck none
🚀 示例与详解:
-
基础使用:
示例:
healthcheck cmd curl --fail http://localhost/ || exit 1
详解:
使用curl
命令检查本地 web 服务是否响应。如果响应失败 (curl --fail
), 命令返回非零状态,这将被视为容器健康检查失败。 -
使用选项:
示例:
healthcheck --interval=5m --timeout=3s --retries=3 \ cmd curl --fail http://localhost/ || exit 1
详解:
每隔5分钟执行一次健康检查,如果健康检查命令超过3秒没有响应, 则视为失败。如果连续3次检查都失败, 则容器状态会变为unhealthy
。
📚 注意事项:
-
如果基础镜像定义了
healthcheck
指令,可以使用healthcheck none
来取消它。 -
健康检查命令应该尽可能地快速执行,以避免误判。
-
健康检查命令的输出可以帮助排查健康检查失败的原因。这些输出可以使用
docker inspect
命令来查看。 -
如果容器的主进程挂掉,容器会立即变为
unhealthy
,不需要等待健康检查。 -
healthcheck
是 docker 1.12 之后引入的功能,确保使用的 docker 版本支持此指令。
🛠️ onbuild 为他人作嫁衣裳
🖋️ 格式:
onbuild
指令后面跟随的是另一个 dockerfile 指令。这个指令不会在当前的镜像构建中执行,但会在使用当前镜像作为基础镜像的任何后续构建中执行。
onbuild <instruction>
🚀 示例与详解:
-
基础镜像 dockerfile:
from node:slim run mkdir /app workdir /app onbuild copy ./package.json /app onbuild run [ "npm", "install" ] cmd [ "npm", "start" ]
详解:
这是一个 node.js 基础镜像。它创建了一个工作目录/app
并设置了默认的启动命令。然而,与其立即执行npm install
,不如等到一个衍生镜像构建时再执行。这正是onbuild
指令的功能。 -
衍生镜像 dockerfile:
from my-node
详解:
当这个 dockerfile 被用来构建一个镜像时,它实际上是基于my-node
镜像。由于my-node
镜像包含onbuild
指令,所以当这个衍生镜像被构建时,onbuild
之后的指令(即copy
和run
)会被触发。
📚 注意事项:
-
预测性: 使用
onbuild
可能会导致镜像构建的不可预测性,尤其是当你不确定基础镜像中的onbuild
指令是什么时。 -
透明性: 如果你使用了一个包含
onbuild
指令的基础镜像,务必确保你知道这些指令的具体内容和行为。 -
过度使用: 避免过度使用
onbuild
。尽管它可以使某些情境变得简单,但不恰当的使用可能会带来更多的复杂性。
🏷️ label 为镜像添加元数据
🖋️ 格式:
label <key1>=<value1> <key2>=<value2> ...
🚀 示例与详解:
-
设置作者和版本信息:
示例:
label maintainer="john.doe@example.com" version="1.0"
详解:
为镜像设置maintainer
和version
两个标签,分别表示维护者的邮箱和镜像的版本。 -
设置多个标签:
示例:
label description="this is a custom image" usage="docker run <image>" version="1.0"
详解:
设置了三个标签:描述、使用方法和版本。 -
设置开放容器标准标签:
示例:
label org.opencontainers.image.authors="john.doe@example.com" \ org.opencontainers.image.documentation="https://example.com/docs"
详解:
使用开放容器标准的标签,表示作者和文档的地址。
📚 注意事项:
-
label
是用来为镜像添加元数据的。这些元数据对构建或运行镜像没有直接影响,但可以为使用和维护镜像的人提供有用的信息。 -
label
的值可以在后续的dockerfile
指令中使用,也可以在运行容器时使用。 -
如果需要更新
label
的值,需要重新构建镜像。 -
使用开放容器标准的标签可以提高跨不同容器技术的互操作性。
-
label
也可以在docker build
命令中通过--label
选项来设置。
🐚 shell 指令
🖋️ 格式:
shell ["executable", "parameters"]
🚀 示例与详解:
-
设置默认 shell:
示例:
shell ["/bin/sh", "-c"]
详解:
这是 dockerfile 在 linux 中的默认 shell。它使用/bin/sh
作为 shell,并使用-c
选项来执行后续的命令。 -
修改 shell 参数:
示例:
shell ["/bin/sh", "-cex"]
详解:
使用-cex
选项,-c
是传递命令的参数,-e
是遇到错误时退出,-x
是打印出执行的每条命令。 -
shell 与 run:
示例:
shell ["/bin/sh", "-cex"] run lll ; ls
详解:
由于shell
指令已经将 shell 设置为/bin/sh
并添加了-cex
参数,因此run
指令在执行时会打印出每个命令,并在遇到错误时退出。 -
shell 与 entrypoint 和 cmd:
示例:
shell ["/bin/sh", "-cex"] entrypoint nginx cmd nginx
详解:
当entrypoint
和cmd
指令以 shell 格式指定时,shell
指令所指定的 shell 也会成为这两个指令的 shell。这意味着entrypoint
和cmd
指令都会在/bin/sh
下执行,并使用-cex
参数。
📚 注意事项:
-
shell
指令会影响后续的所有run
、cmd
和entrypoint
指令。 -
如果需要在特定的
run
、cmd
或entrypoint
指令中使用不同的 shell 或参数,你需要在该指令中明确指定。 -
更改
shell
会持续影响后续的所有层,除非再次更改。 -
在构建多阶段 dockerfile 时,每个阶段都可以有自己的
shell
指令。 -
shell
指令主要用于当你需要使用不同的 shell(例如,/bin/bash
)或默认 shell 的不同参数时。
🌐参考文档
-
dockerfile 官方文档:
- 链接: https://docs.docker.com/engine/reference/builder/
- 描述: 这是 docker 的官方文档,详细描述了 dockerfile 的各种指令和其用法。对于希望深入了解 dockerfile 的读者来说,这是一个必读的资源。
-
dockerfile 最佳实践文档:
- 链接: https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
- 描述: 这份文档提供了关于如何编写高质量 dockerfile 的最佳实践。它涵盖了一系列的推荐做法,帮助开发者避免常见的错误和陷阱。
-
docker 官方镜像 dockerfile:
- 链接: https://github.com/docker-library/docs
- 描述: 这是 docker 官方镜像的 github 仓库,其中包含了许多流行软件的 dockerfile。对于希望学习如何为特定软件编写 dockerfile 的读者来说,这是一个很好的参考资源。
📊 dockerfile 多阶段构建
在 docker 17.05 版本之前,我们构建 docker 镜像时,通常会采用两种方式:全部
和 分散
-
全部放入一个 dockerfile 📄
- 优点: 所有的构建过程都包含在一个 dockerfile 中。
- 缺点:
- 镜像层次多,镜像体积较大,部署时间变长 🐢
- 源代码存在泄露的风险 🔓
# dockerfile.one 文件 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"]
构建镜像:
$ docker build -t go/helloworld:1 -f dockerfile.one .
-
分散到多个 dockerfile 📂
- 优点: 可以很好地规避第一种方式存在的风险。
- 缺点: 明显部署过程较复杂 🛠
# dockerfile.build 文件 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 文件 from alpine:latest run apk --no-cache add ca-certificates workdir /root/ copy app . cmd ["./app"]
构建脚本:
# build.sh 文件 #!/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
运行脚本:
$ chmod +x build.sh $ ./build.sh
对比两种方式生成的镜像大小:
$ 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
-
使用多阶段构建 🚀
- 优点: 体积小且避免了上述的问题。
# dockerfile 文件 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"] ...
构建镜像:
$ docker build -t go/helloworld:3 .
对比三个镜像大小:
$ 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
来为某一阶段命名:from golang:alpine as builder
构建指定阶段的镜像:
$ docker build --target builder -t username/imagename:tag .
-
构建时从其他镜像复制文件 🔄
可以复制任意镜像中的文件:
copy --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf
📝 总结: 多阶段构建为我们提供了一个优雅且高效的方式来构建和部署 docker 镜像。
⚙️ 构建多种系统架构支持的 docker 镜像
在构建多种系统架构支持的 docker 镜像时,我们可以使用 docker manifest 来管理不同架构的镜像,并使 docker 引擎能够自动选择合适的镜像。
以下是完整的步骤和示例:
-
🏗️ 构建镜像
首先,在不同的系统架构下构建不同的镜像。例如,在 linux x86_64 架构下构建
username/x8664-test
镜像,以及在 linux arm64v8 架构下构建username/arm64v8-test
镜像。构建并推送这些镜像到 docker hub。 -
📋 创建 manifest 列表
使用
docker manifest create
命令创建一个 manifest 列表,将不同架构的镜像关联起来。在这个示例中,我们将创建一个username/test
的 manifest 列表,关联了username/x8664-test
和username/arm64v8-test
镜像。$ docker manifest create username/test \ username/x8664-test \ username/arm64v8-test
-
📝 设置 manifest 列表
使用
docker manifest annotate
命令设置 manifest 列表的属性,包括操作系统和架构。对于不同的镜像,需要分别设置其属性。$ docker manifest annotate username/test \ username/x8664-test \ --os linux --arch x86_64
$ docker manifest annotate username/test \ username/arm64v8-test \ --os linux --arch arm64 --variant v8
-
👀 查看 manifest 列表
使用
docker manifest inspect
命令来查看 manifest 列表的配置和信息。$ docker manifest inspect username/test
-
🚀 推送 manifest 列表
最后,将 manifest 列表推送到 docker hub,以便 docker 引擎可以自动根据系统架构选择合适的镜像。
$ docker manifest push username/test
-
🧪 测试
现在,在不同的系统架构下运行
docker run
命令来使用username/test
镜像,docker 引擎会自动选择合适的镜像运行。$ docker run -it --rm username/test
这样,你可以在不同的系统架构上轻松使用相同的 manifest 列表来管理多种镜像,而无需手动区分不同的镜像名称。
🔥官方博客: 详细了解 manifest 可以阅读 docker 官方博客 获取更多信息。
🔄 其它制作镜像的方式
当你使用 docker 构建和管理镜像时,有多种方法可供选择,以满足特殊需求和历史原因。除了标准的 dockerfile 构建方法外,以下是其他两种方法:
📦 从 rootfs 压缩包导入
使用 docker import
命令,你可以从 rootfs 压缩包导入镜像。这个压缩包可以是本地文件、远程 web 文件,甚至可以从标准输入中获取。压缩包将在镜像的根目录中展开,并直接成为镜像的第一层。
例如,如果我们想要创建一个名为 openvz/ubuntu:16.04
的 ubuntu 16.04 镜像,可以使用以下命令:
$ docker import \
http://download.openvz.org/template/precreated/ubuntu-16.04-x86_64.tar.gz \
openvz/ubuntu:16.04
这条命令会自动下载 ubuntu-16.04-x86_64.tar.gz
文件,然后将其解压并导入为名为 openvz/ubuntu:16.04
的镜像。
📤 docker 镜像的导入和导出
docker 还提供了 docker save
和 docker load
命令,用于将镜像保存为归档文件,然后在另一个位置加载。尽管这是在没有 docker registry 时的一种做法,但不再被推荐使用。现在,镜像迁移应该直接使用 docker registry,无论是使用 docker hub 还是内部私有 registry。
保存镜像
使用 docker save
命令可以将镜像保存为归档文件。例如,如果要保存名为 alpine
的镜像:
$ docker image ls alpine
$ docker save alpine -o filename
filename
可以是任意文件名,但其实质是一个归档文件。如果希望使用 gzip 压缩,可以使用以下命令:
$ docker save alpine | gzip > alpine-latest.tar.gz
加载镜像
要加载已保存的镜像,可以使用 docker load
命令。例如:
$ docker load -i alpine-latest.tar.gz
这会将镜像加载回 docker 中。如果要从一个机器迁移到另一个机器,还可以使用 ssh 结合 docker save
和 docker load
命令,甚至可以使用工具如 pv
来显示进度条。
这些方法可以用于特殊情况下的镜像导入和导出,但在实际生产环境中,通常建议使用 docker registry 进行镜像的管理和迁移。 🚢
发表评论