站内链接:

介绍

镜像和容器

docker基础知识中我们了解到如下一个工作流:

  1. 构建镜像, 设置 tags, 推送到远程仓库
  2. 使用方 pull 镜像到本地并存储
  3. 用户基于镜像 run 运行一个容器, 可以对容器进行 start, stop, restart 等操作
  4. 用户可以在容器的基础上 commit 产生一个新的镜像, 并由此重新进入 1 步骤

docker 解析器在分析 dockerfile 时:

  1. 根据基础镜像运行一个匿名容器
  2. 逐行运行保留字指令, 若指令会对容器产生改动则会自动进行 commit 生成一个新的镜像层, 在新的镜像层基础上再运行新的匿名容器, 所以不存在两条 RUN 命令, 第一条使用 cd 命令, 第二条就进入 cd 命令下了, 因为每一条命令都会生成一个新的分层存储, 在第一次执行RUN cd /app时仅仅当前进程的工作目录变更, 仅仅是内存层面的变化, 当第二次执行 RUN 命令的时候, 上下文环境又变为 workdir 或者默认的上下文目录了.
  3. 开始解析下一个保留字指令, 流程回到步骤 2, 直到所有指令解析结束, 生成最终的镜像.

从应用角度考虑 dockerfile, docker 镜像, docker 容器三者的作用:

  • Dockerfile 面向开发, 类似源代码
  • Docker 镜像是软件的交互品, 在编译链环境中对源代码进行处理后产生的二进制文件
  • Docker 容器则表示软件的运行态

从面向对象的思维考虑:

  • 镜像相当于类概念
  • 容器就相当于实例, 一个镜像可产生多个运行态的容器实例对象.

从功能角度考虑 dockerfile 中的指令, 可以分为四个部分:

  • 基础镜像信息: From
  • 维护者信息: MAINTAINER
  • 镜像操作指令: RUN, COPY, ADD, EXPOSE, WORKIDIR, ONBUILD, VOLUME, USER 等
  • 容器启动时执行指令: CMD, ENTRYPOINT 等

构建和上下文

docker build 的上下文就是 build 命令中指定的 PATH 或者 URL 路径下的一组文件, 其中 URL 可以是 git 地址, 基于该上下文可以在 Dockerfile 中进行上下文中各类资源的指令操作, 比如 ADD, COPY, RUN 等操作. 例如

1
2
3
4
# 1. 指定PATH为当前目录
docker build .
# 2. 指定特定目录为上下文
docker build contentdirectory

在构建开始是会看到如下的命令行输出Sending build context, 即将本地的上下文环境下的所有资源打包并发送给 docker daemon 引擎守护进程, 整个 dockerfile 的解析动作都是在守护进程引擎中进行的, 下面就是 docker 总体逻辑流程图(早期的 docker 框架简易图):

dockerFrame

docker 命令的执行是基于 CS 架构, 其中 build 命令就会将当前上下文中的所有文件打包并 send 到引擎中以进行镜像的构建. 注意, 从Docker 1.11开始, docker 引擎就开始尽可能遵循 OCI 规范, 并将 daemon 尽量轻量化, 将容器的执行逻辑放到 containerd 中, 其中 runc 就是 OCI 容器运行时规范的参考实现.

保留字

构建镜像

docker build会基于 dockerfile 和构建上下文进行镜像的构建, 关于 build 流程的详细介绍见上文说明. 在镜像构建过程中涉及到的指令有基础镜像, 维护者, 镜像操作指令, 下面简单的介绍一下一些常用的指令.

  1. 基础镜像和构建者
  • From: 基础镜像, 格式可为: From <image>[@<digest>][:<tag>] [AS <name>]
  • Maintainer: 镜像维护者 name 和 email, 目前已被 label 替代, 流入: LABEL maintainer="xxx"
  • LABEL: 给构建的镜像打标签, 例如LABEL version="1.0", 注意, 每一个标签都会生成一个镜像层

例如, 在一个已有镜像上设置一个新的维护者, 不增加其他任何镜像信息

1
2
FROM python:3
MAINTAINER unusebamboo@163.com

其构建命令docker build -t bamboopython:1.0 ., 之后可以启动容器 python 交互界面:

1
2
3
4
5
# 1. 此时会弹出python解释器终端交互界面.
docker run --rm -it bamboopython:1.0

# 2. 该容器还支持bash, 可以进入容器中进行命令行动作, 在退出时自动清理容器
docker run --rm -it bamboopython:1.0 bash
  1. 在基础镜像的基础上增加一些新的存储层或命令, 涉及的命令
  • Copy: 拷贝本地路径源文件到镜像中指定位置, 功能相比 ADD 较弱
  • Add: 拷贝本地源文件以及远程 URL 文件到镜像中指定位置, 如果是归档文件还会自动解压
  • Run: 运行命令, 两种格式 shell 格式和 exec 格式, 见下文说明
  • ONBUILD: 向镜像中添加一个触发器, 当以此镜像构建新镜像时会执行触发器指定的指令, 不常用
  • SHELL: 更改后面的指令中所使用的的 shell, 默认的 shell 为["/bin/sh", "-c"], 这个也不常用

其中 RUN 命令格式有两种风格, 分别是 shell 格式和 exec 格式:

1
2
3
4
# 1. shell格式: RUN shellcmd
RUN apt install -y vim
# 2. exec格式: RUN ['cmd', 'arg1', 'arg2']
RUN ['apt', 'install', '-y vim']

ADD 命令类似 RUN 命令, 其也存在两种格式, 换一种说法, dockerfile 中的 Commmand 格式都差不多, 注意, ADD 中的 src 可以是文件, 目录, URL, 可以指定多个 src, 甚至可以使用模糊匹配从当前上下文中添加文件到镜像中.

1
2
3
4
# 1. 格式1: add <src> <dest>, 其中src都是在构建上下文中, 关于上下文见上文说明
ADD djangoweb /workspace
# 2. 格式2: add ["src", "dest"], 该用法对包含空格的路径适用
ADD ["djangoweb" "/workspace"]

例如, 一个配置 django 基础运行环境的 dockerfile

1
2
3
4
5
6
7
8
# 功能: 搭建django运行基础环境
FROM python:3
ENV PYTHONUNBUFFERED 1
# 创建目录(不存在时自动创建)并将当前工作目录(上下文目录)设置为workspace
WORKDIR /workspace
# 这里会将djangoweb目录下的文件打包拷贝到workspace而非直接将目录拷贝过去
ADD djangoweb /workspace
RUN pwd && ls && pip install -r requirements.txt

注意, 拷贝目录的时候仅仅拷贝目录中的文件, 而不会将整个文件拷贝到工作目录中, 比如:

1
2
3
4
# 1. 将djangoweb下的所有文件拷贝到/workspace下
COPY djangoweb /workspace
# 2. 将djangoweb目录拷贝
COPY djangoweb /workspace/djangoweb
  1. 调试 dockerfile

在刚刚写好 dockerfile 之后进行 build 的过程中可能会碰到各种错误, 此时默认的 build 命令无法进行调试, 此时就需要 build 命令的一些 options 才能查出问题所在

  • -f file1: 指定 dockefile 路径
  • --no-cache: 调试错误的时候非常有用, 每次都重新下载并重新开始 build
  • --quiet: 安静模式, 这是默认值
  • --progress=auto/plain/tty: 设置进度输出的类型, 默认为 auto, 若是调试时可以设置为 plain

其他 options 选项这里就详细介绍. 为了找出问题所在, 可以在 Dockerfile 中增加一些调试 RUN 命令再搭配命令docker build --progress=plain --no-cache -f myDockerfile -t mydjango:1.0 .来达到目的.

  1. onbuild 用途

在存在一个基础镜像下, 基于该基础镜像的其他衍生镜像若存在相同的命令(类中相同的虚函数), 这些命令不能在基础镜像构建的时候执行, 这时候就会造成衍生镜像重复编写指令, 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 1. 基础镜像
FROM node:slim
RUN mkdir /app
WORKDIR /app
CMD [ "npm", "start" ]

# 2. 衍生镜像1
FROM my-node
COPY ./package.json /app
RUN [ "npm", "install" ]
COPY . /app/

# 3. 衍生镜像2
FROM my-node
COPY ./package.json /app
RUN [ "npm", "install" ]
COPY . /app/

这样就会造成重复编写工作, ONBUILD 指令就是为这种情况准备的, 此时基础镜像可以编写成下面的指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
FROM node:slim
RUN mkdir /app
WORKDIR /app
ONBUILD COPY ./package.json /app
ONBUILD RUN [ "npm", "install" ]
ONBUILD COPY . /app/
CMD [ "npm", "start" ]

# 衍生镜像1:
From my-node

# 衍生镜像2
From my-node

运行容器

镜像启动过程中涉及的指令有:

  • CMD: 指定容器启动后的要运行的命令或者命令参数, 其也有两种命令风格: shell, exec
  • ENV: 构建镜像过程中设置环境变量, 这些环境变量可以为后续的 RUN 指令使用, 同时在容器中也存在
  • ARG: 仅在构建镜像时设置环境变量, 在容器中不存在设置的变量值, 可以通过docker history查看
  • EXPOSE: 容器对外暴露出的端口
  • VOLUME: 容器数据卷命令
  • ENTRYPOINT: 指定容器启动后的要干的事情, 其中 shell 命令风格好像无法复用 CMD 命令, 不知道为何

这里重点介绍下 CMD 和 Entrypoint 命令:

  • 一个 dockerfile 中可以有多个 CMD 命令, 但是只有最后一个才生效(ENTRYPOINT 也存在该特点)
  • CMD 命令会被docker run命令之后的参数替换(重要), ENTRYPOINT 则不会被 run 命令参数覆盖
  • CMD 和 ENTRYPOINT 是在容器启动时运行的命令, RUN 是在 build 的时候运行的命令
  • 一旦存在 Entrypoint, 则 CMD 和docker run都会作为 Entrypoint 的参数, 这点非常重要

针对以上四点, 我们相应的做一些验证测试.

  1. 多个 cmd 命令和参数替换
1
2
3
4
5
6
FROM python:3
MAINTAINER unusebamboo@163.com

# a. 测试CMD最后一条生效
CMD ls && pwd
CMD echo "xxxxxxxxxxlast commandxxxxxxxxxx"

在运行容器前要明确一下, 什么是 run 指令后的参数, 一个 docker run 指令可以分成如下部分:

`docker run 指令选项(-d, -it) 镜像名 参数(bash, 命令等)`

注意到了吗, 这个逻辑作者本人也是很久才理清楚的, 那么 CMD 命令会被参数覆盖就是会被镜像名后面的命令覆盖, 此时, 使用不同 run 命令是有不同的输出的:

1
2
3
4
5
6
7
8
9
# 1. 正常输出echo命令中的信息, 当然ls && pwd的输出则没有, 证明了上面的CMD仅仅最后一条生效的说明
docker run --rm --name mypyenv mypyenv:1.0
docker run --rm -it --name mypyenv mypyenv:1.0

# 2. 增加参数之后会覆盖CMD命令
# 执行ls而非CMD命令
docker run --rm --name mypyenv mypyenv:1.0 ls -l /usr/
# 进入容器交互终端
docker run --rm -it --name mypyenv mypyenv:1.0 bash
  1. entrypoint 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
FROM python:3
MAINTAINER unusebamboo@163.com

# a. 测试CMD最后一条生效
CMD ls && pwd
# CMD ["echo", "xxxxxxxxxxlast commandxxxxxxxxxx"]
CMD echo "xxxxxxxxxxlast commandxxxxxxxxxx"

# b. 测试ENTRYPOINT存在时cmd作为参数, 此时若CMD不是正常参数可能有异常输出
# --> shell方式无法复用CMD和run后面参数, 为何?
# ENTRYPOINT echo
# --> 建议先采用exec方式
ENTRYPOINT ["echo"]

通过命令docker build -t mypyenv:2.0 -f EntryDockerfile .进行镜像构建, 下面是一些容器的测试和输出

1
2
3
4
5
6
7
8
# 1. 不传递run参数, 直接复用CMD: /bin/sh -c echo "xxxxxxxxxxlast commandxxxxxxxxxx", 观察输出,
# 其会将完整的CMD命令传递给ENTRYPOINT, 所以建议使用exec格式编写CMD和ENTRYPOINT
docker run --rm --name mypyenv mypyenv:2.0

# 2. 若上面的CMD使用exec方式, 则输出变为: echo xxxxxxxxxxlast commandxxxxxxxxxx

# 3. 测试run命令后参数, 输出: bash
docker run --rm --name mypyenv mypyenv:2.0 bash

所以, 对于配置了 ENTRYPOINT 的镜像, 其无法通过 bash 进入交互终端, 其从根源上避免了此操作. 一个常用的 ENTRYPOINT 配置如下:

1
2
CMD	["/etc/nginx/nginx.conf"]
ENTRYPOINT ["nginx", "-c"]

这也是 ENTRYPOINT 提出的目的:

  • a. 让镜像变成像命令一样使用, 若是不满意默认的 nginx 配置文件还可以在启动容器的时候携带自定义的参数.
  • b. 服务启动前的准备工作, 这是最通用的用法, 其一般搭配docker-entrypoint.sh使用

下面是一个使用docker-entrypoint.sh的例子:

1
2
3
4
5
6
7
8
FROM python:3
MAINTAINER unusebamboo@163.com
# 确认docker-entrypoint.sh可执行, 这里放到默认的workdir目录下
ADD docker-entrypoint.sh .

# a. 标准的entrypoint命令使用, 以python服务说明, 注意docker-entrypoint的可执行和路径
CMD ["python"]
ENTRYPOINT ["./docker-entrypoint.sh"]

通过命令docker build -t mypyenv:3.0 -f EntryShDockerfile .进行构建, 之后容器运行测试例子如下:

1
2
3
4
5
6
# 1. 不携带参数, 在运行entrypoint.sh之后自动运行CMD, 注意, 对于python交互需要增加参数: -it
docker run --rm -it --name mypyenv mypyenv:3.0

# 2. run参数, 此时不会执行CMD命令
docker run --rm -it --name mypyenv mypyenv:3.0 bash
docker run --rm -it --name mypyenv mypyenv:3.0 pwd

通用命令

在构建阶段和运行阶段皆涉及的指令:

  • WORKDIR: 指定工作目录, 在 BUILD 阶段就是上下文环境目录, 在容器运行阶段执行的命令也会在该 Workdir 目录下执行, 当然, 容器运行的时候可以通过参数-w覆盖该值, 默认工作目录为/
  • USER: 镜像以什么样的用户去执行, 默认为 root
  • SHELL: 格式为shell ['executable', 'parameters'], 用于指定 RUN, ENTRYPOINT, CMD 指令的 shell, 默认的 shell 为["/bin/bash", "-c"], 其中-c表示 bash 命令将从后续的字符串中读取
  1. SHELL 指令测试
1
2
3
4
5
6
7
# 1. 可以有多条SHELL, 其生效范围为后续第一条指令下一个SHELL指令之间的所有指令
SHELL ["/bin/sh", "-c"]
RUN ls

# 2. 设置新的SHELL, 其会在指令碰到错误的时候退出, -e 或者 set -e会让bash的任何一个语句返回非真时退出bash
SHELL ["/bin/sh", "-cex"]
RUN lll ; ls

参考