docker镜像制作

本文最后更新于:2023年12月5日 晚上

docker 镜像生命周期

制作镜像方法:

  • 手工制作(基于容器)
  • 自动制作(基于 dockerfile),企业通常都是基于 dockerfile 制作镜像

手动构建镜像 commit

将现有容器通过 docker commitdocker container commit 手动构建镜像

根据容器的更改创建新镜像:

# docker container commit --help
Usage: docker container commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]

Options:
  -a, --author string    作者 (e.g., "John Hannibal Smith <hannibal@a-team.com>")
  -c, --change list      使用Dockerfile指令来创建镜像
  -m, --message string   提交时的说明文字
  -p, --pause            在commit时,将容器暂停 (default true)

说明:

  • 制作镜像和容器的状态无关,停止状态也可以制作
  • 如果没有指定[REPOSITORY[:TAG]],REPOSITORY 和 TAG 都为<none>
  • 提交的时候标记 TAG,后期可以根据 TAG 标记创建不同版本的镜像以及创建不同版本的容器

具体步骤

  1. 下载一个官方的基础镜像,例如:centos、ubuntu、alpine
  2. 基于基础镜像启动一个容器,并进入
  3. 在容器里面进行安装服务、修改配置等操作
  4. 提交一个新镜像 docker container commit
  5. 基于自己的镜像创建容器并访问、

案例:基于 alpine 基础镜像制作 nginx 镜像

  1. 下载最新版 alpine 基础镜像

    root@Z510:~# docker pull alpine
  2. 启动 alpine 并进入

    root@Z510:~# docker container run -it alpine
    / #
  3. 另开一个终端,将 shell 脚本拷贝到 alpine 镜像

    lujinkai@Z510:~/www/script$ sudo docker container ls
    CONTAINER ID  IMAGE   COMMAND    CREATED             STATUS    PORTS    NAMES
    b8de1bdb6c88  alpine  "/bin/sh"  About a minute ago  Up About a minute  funny_chatelet
    
    lujinkai@Z510:~/www/script$ sudo docker container cp -a ./alpine-docker/ b8de:/root
  4. 回到容器,运行 shell 脚本

    root@Z510:~# docker container run -it alpine
    / # cd
    ~ # ls
    alpine-docker
    ~ # cd alpine-docker/
    ~/alpine-docker # ./install.sh
    ....
    make[1]: Leaving directory '/root/alpine-docker/src/nginx-1.18.0'
    Nginx installed successfully!
    ~/alpine-docker # exit
  5. 提交镜像

    lujinkai@Z510:~$ sudo docker container commit \
        -a 'lujinkai<root@lujinkai.com>' \
        -c 'EXPOSE 80' \
        -c 'CMD ["/usr/local/nginx/sbin/nginx"]' \
        b8de1bdb6c88 nginx-alpine:v1
    lujinkai@Z510:~$ sudo !!
    sudo docker image ls
    REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
    nginx-alpine        v1                  de56d80c5c8c        14 seconds ago      340MB
    alpine              latest              d6e46aa2470d        4 weeks ago         5.57MB
  6. 启动基于新制作镜像的容器

    root@Z510:~# docker container run -d -p 80:80 nginx-alpine:v1
    ebbe778a7fe303e6a5cb840203d95a7b5e2a07208365b71f784ddc8357a664d7
    root@Z510:~# curl 127.0.0.1:81
    hello nginx

  7. iptables

    root@Z510:~# iptables -t nat -nL
    ...
    Chain DOCKER (2 references)
    target     prot opt source  destination
    RETURN     all  --  anywhere anywhere
    DNAT       tcp  --  0.0.0.0/0 0.0.0.0/0  tcp dpt:81 to:172.17.0.2:80
  8. ss

    root@Z510:~# ss -ntl
    State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
    ...
    LISTEN 0  4096  *:81     *:*
    ...
  9. 有需求的话,可以导出镜像:docker image save [OPTIONS] IMAGE

自动构建镜像

docker builder build 从 DockerFile 文件中构建镜像

docker builder build

docker builder builddocker build 命令用来基于 dockerfile 构建镜像

docker builder build [OPTIONS] PATH | URL | -

PATH | URL | -
上下文目录,如果设置为 - ,则从标准输入获取 dockerfile 的内容,什么是上下文目录?举个例子:COPY 指令复制文件时,源文件只能在上下文目录

OPTIONS
   -f, --file string    指定Dockerfile文件,默认为上下文目录下的 Dockerfile
    --force-rm     总是删除中间层容器,创建镜像失败时,删除临时容器
    --no-cache     不使用之前构建中创建的缓存
    --rm=true     创建镜像成功时,删除临时容器
 -c, --cpu-shares int   设置 cpu 使用权重,按照比例分配cpu资源,当只有一个容器时,`--cpu-shares` 选项没有意义。
   -m, --memory bytes    设置构建内存上限
   -t, --tag list     为构建的镜像打上标签
   -q, --quiet      不显示构建过程的信息
docker build .
docker build /usr/local/src/nginx
docker build -f /path/to/a/Dockerfile .
docker build -t shykes/myapp .
docker build -t shykes/myapp:1.0.2 -t shykes/myapp:latest .
docker build -t test/myapp .
docker build -t nginx:v1 /usr/local/src/nginx

docker builder build -t nginx:v1 .    # 构建一个镜像,指定上下文目录为当前目录

Dockerfile 介绍

DockerFile 是一种被 Docker 程序解释执行的脚本,由一条条的命令组成的,每条命令对应 linux 下面的一条命令,Docker 程序将这些 DockerFile 指令翻译成真正的 linux 命令,其有自己的书写方式和支持的命令,Docker 程序读取 DockerFile 并根据指令生成 Docker 镜像,相比手动制作镜像的方式,DockerFile 更能直观的展示镜像是怎么产生的,有了 DockerFile,当后期有额外的需求时,只要在之前的 DockerFile 添加或者修改响应的命令即可重新生成新的 Docker 镜像,避免了重复手动制作镜像的麻烦,类似与 shell 脚本一样,可以方便高效的制作镜像。

Docker 守护程序 Dockerfile 逐一运行指令,如有必要,将每个指令的结果提交到新镜像,然后最终输出新镜像的 ID。Docker 守护程序将自动清理之前发送的上下文。

请注意,每条指令都是独立运行的,并会导致创建新镜像,比如 RUN cd /tmp 对下一条指令不会有任何影响。

Docker 将尽可能重用中间镜像层(缓存),以显著加速 docker builder build 命令的执行过程,这由 Using cache 控制台输出中的消息指示。

Dockerfile 镜像制作和使用流程

Dockerfile 文件的制作镜像的分层结构

# 按照业务类型或系统类型等方式划分创建目录环境,方便后期镜像比较多的时候进行分类
[root@ubuntu1804 ~]$ mkdir
/data/dockerfile/{web/{nginx,apache,tomcat,jdk},system/{centos,ubuntu,alpine,debian}} -p
[root@ubuntu1804 ~]$ tree /data/dockerfile/
/data/dockerfile/
├── system
│├── alpine
│├── centos
│├── debian
│└── ubuntu
└── web
├── apache
├── jdk
├── nginx
└── tomcat
10 directories, 0 files

build 构建过程

  1. 从基础镜像运行一个容器
  2. 顺序执行一条指令对容器做出修改
  3. 执行类似 docker commit 的操作提交一个新的镜像层(可以利用中间层镜像创建容器进行调试和排错)
  4. docker 基于刚才提交的镜像运行一个新的容器
  5. 执行 Dockerfile 中的下一条指令,直至所有指令执行完毕

Dockerfile 文件格式

Dockerfile 是一个有特定语法格式的文本文件

  • 每一行以 Dockerfile 的指令开头,指令不区分大小写,但是惯例使用大写
  • 使用 # 开始作为注释
  • 每一行只支持一条指令,每条指令可以携带多个参数
  • 指令按文件的顺序从上至下进行执行
  • 每个指令的执行会生成一个新的镜像层,为了减少分层和镜像大小,尽可能将多条指令合并成一条指令
  • 制作镜像一般可能需要反复多次,每次执行 Dockerfile 都按顺序执行,从头开始,已经执行过的指令已经缓存,不需要再执行,如果后续有一行新的指令没执行过,其往后的指令将会重新执行,所以为加速镜像制作,将最常变化的内容放下 Dockerfile 的文件的后面

Dockerfile 相关指令

docker builder build 命令基于 dockerfile 构建镜像,docker container run 命令基于镜像启动容器

所以 dockerfile 的指令也分为三类:

  • docker builder build 阶段运行
  • docker container run 阶段运行
  • 在以上两个阶段都运行,这种命令都是“设置”相关的,例如 USER 设置用户、ENV 设置环境变量、WORKDIR 设置工作目录,它们的设置在 build 和 run 两个阶段都生效

FROM

FORM 指定基础镜像,此指令通常必需放在 Dockerfile 文件第一个非注释行。后续的指令都是运行于此基准镜像所提供的运行环境

FROM [--platform=<platform>] <image>[:<tag>]

关于 scratch 镜像:

该镜像是一个空的镜像,可以用于构建 busybox 等超小镜像,可以说是真正的从零开始构建属于自己的镜像该镜像在构建基础镜像(例如 debian 和 busybox)或超最小镜像(仅包含一个二进制文件及其所需内容,例如:hello-world)的上下文中最有用

LABEL

LABEL 指定镜像元数据,如 镜像作者等

LABEL <key>=<value> <key>=<value> <key>=<value> ...

示例:

LABEL maintainer="lujinkai <root@lujinkai.com>" \
    version="1.0" \
    other="value3" \
    description="some description" \
    other="value1"

注意:MAINTAINER 指令已过时,使用 LABEL 代替

RUN

RUN 指令用来在 build 阶段需要执行 shell 命令,一定要是继承的镜像所支持的 shell 命令

注意: RUN 可以写多个,每一个 RUN 指令都会建立一个镜像层,所以尽可能使用 && 将多条指令合并为一条

相邻的 RUN 命令相互独立,示例:

RUN cd /app
RUN echo "hello" > world.txt  # world.txt并不存放在/app内

ENV

ENV 定义环境变量和值,会被后续指令通过$KEY或${KEY}进行引用,并在容器运行时保持

ENV <key1>=<value1> \
 <key2>=<value2> \
 <key3>=<value3>

# 变量支持高级赋值格式
${key:-word}
${kye:+word}

docker run 运行容器,如果需要修改环境变量,使用 -e 重新定义即可覆盖

COPY

复制指令,从上下文目录中复制文件或者目录到容器里指定路径

COPY [--chown=<user>:<group>] ["<src>",... "<dest>"] # 如果路径中有空白字符,加双引号
  • --chown=<user>:<group>:改变复制到容器内文件的属主和属组,默认保留所有元数据

  • "<src>"

    • 支持通配符,通配符规则满足 Go 的 filepath.Match 规则
    • 如果是目录,则其内部文件或子目录会被递归复制,但目录自身不会被复制
  • "<dest>"

    • 支持绝对路径 或 WORKDIR 指定的相对路径
    • 如果不存在,会自动创建,递归创建目录
    • 如果"<src>"是多个文件,则"<dest>"必须是目录,且以 / 结尾

范例:

COPY hom* /mydir/
COPY hom?.txt /mydir/

ADD

增强版 COPY,支持自动解压缩,如果只是复制,尽量使用 COPY

支持解压缩(identity、gzip、bzip2、xz),支持解包(tar),支持像解压缩+解包,例如 .tar.gz

注意:ADD 识别压缩文件,取决于文件内容,不取决于文件后缀,如果一个文本文件直接修改后缀为 .tar.gz,该文件将被简单地复制到目标文件

ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]
  • "<src>":支持 URL,下载后的文件权限自动设置为 600,如果下载的是 tar 文件将不会自动展开

示例:

ADD test relativeDir/   # adds "test" to `WORKDIR`/relativeDir/
ADD test /absoluteDir/   # adds "test" to /absoluteDir/
ADD --chown=55:mygroup files* /somedir/
ADD --chown=bin files* /somedir/
ADD --chown=1 files* /somedir/
ADD --chown=10:11 files* /somedir/
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /

CMD

CMD 指定启动容器时默认执行的一个命令

  • 命令运行结束后容器也会停止,所以一般指定持续运行且为前台的命令
  • 每个 Dockerfile 只能有一条CMD 命令。如指定了多条,只有最后一条被执行
  • 如果用户启动容器时用 docker run xxx 指定运行的命令,则会覆盖 CMD 指定的命令
# 使用 exec 执行,推荐方式,第一个参数必须是命令的全路径,此种形式不支持环境变量
CMD ["executable","param1","param2"]

# 在 /bin/sh 中执行,提供给需要交互的应用;此种形式支持环境变量
CMD command param1 param2

# 提供给 ENTRYPOINT 命令的默认参数
CMD ["param1","param2"]

示例:

CMD ["nginx", "-g", "daemon off;"]

ENTRYPOINT

入口点,功能类似于CMD,配置容器启动后执行的命令及参数

  • ENTRYPOINT 不能被 docker run 提供的参数覆盖,而是以追加的形式作为 ENTRYPOINT 的参数
  • 每个 Dockerfile 中只能有一个 ENTRYPOINT,当指定多个时,只有最后一个生效
# 使用 exec 执行
ENTRYPOINT ["executable", "param1", "param2"]

# shell中执行
ENTRYPOINT command param1 param2

CMDENTRYPOINT 都定义了容器运行时的执行命令。如下是它们的一些使用规则:

  • CMDENTRYPOINT 在 Dockerfiles 中应该至少应该有一个被定义
  • 当构建可执行容器时,应该定义 ENTRYPOINT 指令
  • CMD 要么用于给 ENTRYPOINT 提供默认参数,要么用于在容器中执行一个特定命令
  • CMD 可以通过容器启动命令 docker run 的参数来替换它

ARG

ARG 在 build 阶段指定变量,和 ENV 不同的是,容器运行时不会存在这些环境变量

ARG <name>[=<default value>]
  • 如果和 ENV 同名,ENV 覆盖 ARG 变量

  • 可以用 docker build --build-arg <参数名>=<值> 来覆盖

  • FROM 之前声明的 ARG 在构建阶段之外,所以它不能在 FROM 之后的任何指令中使用。 要使用在第一个FROM 之前声明的 ARG 的默认值,请在构建阶段内使用没有值的 ARG 指令

    # 示例:
    ARG VERSION=latest
    FROM busybox:$VERSION
    ARG VERSION
    RUN echo $VERSION > image_version

VOLUME

匿名卷,将宿主机上的目录挂载至 VOLUME 指定的容器目录。即使容器后期被删除,此宿主机的目录仍会保留,从而实现容器数据的持久保存

VOLUME <容器内路径>
VOLUME ["<容器内路径1>", "<容器内路径2>"...]
  • VOLUME 实现的是匿名卷,无法指定宿主机路径和容器目录的挂载关系,宿主机目录位于:

    /var/lib/docker/volumes/
  • 通过docker rm -fv <容器ID> 可以删除容器的同时删除 VOLUME 指定的卷

  • 如果要指定宿主机目录,使用 docker run 的 -v 参数,参考:docker 数据管理

示例:

# 1. dockerfile,生成两个匿名卷
VOLUME [ "/testdata","/testdata2" ]

# 2. 进入容器,测试
cp /etc/issue /testdata/f1.txt
cp /etc/issue /testdata2/f2.txt

# 3. 查看宿主机目录
$ tree /var/lib/containers/storage/volumes/
/var/lib/containers/storage/volumes/
├── 725f0f67921bdbffbe0aaf9b015d663a6e3ddd24674990d492025dfcf878529b
│ └── _data
│ └── f1.txt
└── fbd13e5253deb375e0dea917df832d2322e96b04ab43bae061584dcdbe7e89f2
└── _data
└── f2.txt

EXPOSE

WORKDIR

指定工作目录,即后续 RUNCMDENTRYPOINT 指令的相对路径,如该目录不存在,WORKDIR 会自行创建

WORKDIR /path/to/workdir

示例:

# 两次RUN独立运行,不在同一个目录,
RUN cd /app
RUN echo "hello" > world.txt

# 如果想实现相同目录可以使用WORKDIR
WORKDIR /app
RUN echo "hello" > world.txt

# 可以使用多个 WORKDIR 指令,后续命令如果参数是相对路径,则会基于之前命令指定的路径
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd   # /a/b/c

ONBUILD

ONBUILD 指令在当前镜像 build 阶段不会执行,会在子镜像 build 阶段执行,即:延迟执行

使用 ONBUILD 指令的镜像,推荐在标签中注明,例如 ruby:1.9-onbuild

USER

指定执行后续指令的用户和用户组,后续的 RUN 也会使用指定用户,默认是 root 身份执行

这个用户必须是事先建立好的,否则无法切换

USER <user>[:<group>]
USER <UID>[:<GID>]

HEALTHCHECK

检查容器的健康性

HEALTHCHECK [选项] CMD <命令>  # 设置检查容器健康状况的命令
HEALTHCHECK NONE  # 如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令

--interval=<间隔> #两次健康检查的间隔,默认为 30 秒
--timeout=<时长>  #健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒
--retries=<次数>  #当连续失败指定次数后,则将容器状态视为 unhealthy,默认3次
--start-period=<FDURATION> #default: 0s

示例:

FROM nginx
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
HEALTHCHECK --interval=5s --timeout=3s \
 CMD curl -fs http://localhost/ || exit 1

STOPSIGNAL

退出容器的信号

STOPSIGNAL 设置将被发送到容器退出的系统调用信号。该信号可以是与内核 syscall 表中的位置匹配的有效无符号数字(例如 9),也可以是 SIGNAME 格式的信号名称(例如 SIGKILL)

STOPSIGNAL signal

SHELL

指定 shell,SHELL指令可以出现多次。 每个SHELL指令将覆盖所有先前的SHELL指令,并影响所有后续的指令

SHELL ["executable", "parameters"]

示例:

SHELL ["powershell", "-command"]
SHELL ["cmd", "/S", "/C"]

.dockerignore 文件

与.gitignore 文件类似,生成构建上下文时 Docker 客户端应忽略的文件和文件夹指定模式

示例:

test/*   #排除 test 目录下的所有文件
md/xttblog.md #排除 md 目录下的 xttblog.md 文件
xttblog/*.md #排除 xttblog 目录下的所有 .md 的文件
xttblog?  #排除以 xttblog 为前缀的文件和文件夹
**/*.sql  #排除所有目录下的 .sql 文件夹

#除了README的md不排外,排除所有md文件,但不排除README-secret.md
*.md
!README*.md
README-secret.md

#除了所有README的md文件以外的md都排除
*.md
README-secret.md
!README*.md

补充:如何优化镜像体积?

  1. .dockerignore 只会影响构建的速度,不会影响最终镜像大小;
  2. 相比COPYADD会显著增大镜像体积;
  3. 如果是中间文件,例如源码包,安装后是要删掉的,使用COPY也会增大镜像体积,可以在RUN中使用wget命令下载;
  4. 最好的方法是 多阶段构建:在前面的阶段无需考虑指令对最终镜像体积的影响,在最后阶段只需要从前面的阶段COPY必要的文件即可。

COPY是否加/

现在要将目录node-v20.10.0复制到目录/usr/local下:

FROM alpine:3.18
COPY node-v20.10.0 /usr/local/node-v20.10.0
FROM alpine:3.18
COPY node-v20.10.0 /usr/local/node-v20.10.0/
FROM alpine:3.18
COPY node-v20.10.0/ /usr/local/node-v20.10.0
FROM alpine:3.18
COPY node-v20.10.0/ /usr/local/node-v20.10.0/

以上四种写法均符合预期


docker镜像制作
http://blog.lujinkai.cn/运维/Docker/2.docker镜像制作/
作者
像方便面一样的男子
发布于
2021年1月8日
更新于
2023年12月5日
许可协议