Docker 学习笔记

Table of Contents

1. Docker 简介

Docker 是基于 Linux 内核的 cgroup,namespace,以及 AUFS 类的 Union FS 等技术,对进程进行封装隔离,属于操作系统层面的虚拟化技术。也称之为容器。

VM vs Docker:VM 需要运行一个完整的操作系统,再在系统上运行应用进程;而容器的应用进程直接运行于宿主机的内核,所以相比 VM 更为轻便。

whatisdocker.png

Docker 的优势:

  • 更好的利用系统资源,因为少去了虚拟操作系统的开销,相比 VM 可以运行更多数量的应用
  • 更快的启动时间
  • 一致的运行环境,Docker 镜像提供除内核之外的完整运行环境,确保了应用运行环境一致性
  • 持续交付和部
  • 更轻松的迁移,Docker 几乎可以运行于任意环境,物理机、虚拟机、公有云、私有云等,运行结果是一致的
  • 更轻松的维护和扩展,分层存储和镜像技术,使得易复用

2. 基本概念

2.1. 镜像(Image)

Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。

2.2. 容器(container)

镜像是静态的定义,容器是镜像运行之后的实例。容器可以被创建、启动、停止、删除等。容器的本质是进程,但与直接在宿主机运行的进程不同,它是一套隔离的环境(独立的命名空间):有自己的文件系统、网络配置、进程空间,使用起来就像是一个独立的宿主系统一样。

容器也使用分层存储,容器运行时,镜像时基础层,在其上创建一个当前容器的存储层,称之为容器运行时读写而准备的存储层为容器存储层。它的生命周期和容器一样,因此保存于容器存储层的信息都会随容器的删除而丢失。

按照 Docker 最佳实践的要求,容器不应该像其存储层写入任何数据,保持无状态化。如果有写入操作,应该使用数据卷(Volumn)、或者绑定宿主目录,这些位置的读写将跳过容器存储层,直接对宿主机发生读写,性能和稳定性更高。

2.3. Docker Registry

Registry => Repositories => Tags = Image

仓库是集中存储和分发镜像的服务。一个 Registry 可以包含多个仓库(Repository),每个仓库可以包含多个标签(Tag),每个标签对应一个镜像(Image)。

通常一个仓库对应一个软件,Tag 对应软件的不同版本镜像。可以通过 <仓库名>:<标签> 来指定具体软件版本的镜像,如果不给定标签,默认为 lastest 标签。

3. 安装 Docker

直接用系统的包管理器安装的版本一般比较低,推荐设置源之后,手动安装,具体可以查看 Docker 官方说明:

3.1. 设置加速器

推荐 Azure 的源,AKS on Azure China Best Practices

修改配置 /etc/docker/daemon.json

{
    "registry-mirrors": [
        "https://dockerhub.azk8s.cn"
    ]
}

重启: service docker restart ,查看信息: docker info

其他加速器:

WSL Ubuntu 22.04 启动 docker 报错:

failed to start daemon: Error initializing network controller: error obtaining controller instance: unable to add return
rule in DOCKER-ISOLATION-STAGE-1 chain:  (iptables failed: iptables --wait -A DOCKER-ISOLATION-STAGE-1 -j RETURN:
iptables v1.8.7     (nf_tables):  RULE_APPEND failed (No such file or directory): rule in chain DOCKER-ISOLATION-STAGE-1

解决办法1

sudo update-alternatives --set iptables /usr/sbin/iptables-legacy
sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy

4. 镜像操作

  • 获取镜像: docker pull
  • 运行镜像: docker run -it --rm image:tag bash
    • -it: -i 交互式操作, -t 指终端
    • --rm: 容器退出后删除,否则需要手动执行 docker rm
    • bash: 放在镜像最后的是命令
  • 列出本地镜像: docker image ls 或者 docker images ,内容分别对应: 仓库名、标签、镜像ID、创建时间、所占用的空间,镜像 ID 是镜像的唯一标识
    • docker image ls -a 显示所有镜像(包括中间层镜像),顶层的镜像会依赖中间层镜像,当没有顶层镜像依赖时,中间层会连带删除
    • docker images ubuntu 可以做镜像筛选,使用 -f / --filter 选项做更为复杂的筛选,比如 since=mongo:3.2 表示 3.2 之后建立的镜像
    • docker image ls -q 只显示镜像 ID,在批量删除镜像时很有用
  • docker system df: 查看镜像、容器、数据卷所占用的空间
  • docker image ls -f dangling=true 显示所有的悬挂镜像(dangling), docker image prune 可以删掉他们
  • 删除本地镜像: docker image rm 或者 docker rmi
    • docker rmi $(docker image ls -q redis): 批量删除所有的 redis 镜像

4.1. 理解镜像构成

docker diff 可以查看容器被修改的文件(也就是修改了容器的存储层),Docker 提供了 docker commit 命令,用于在原有镜像的基础上,叠加修改部分,构成新的镜像。 提交命令有点像 git commit ,具体可使用 docker commit --help 来查看格式和参数。 docker history 可以查看历史记录。

commit 可能会把一些无关紧要的文件(安装包、构建临时文件等)全部加进来,处理的不好,会导致镜像臃肿。分层机制是对前一层进行追加,而不是改动,所以每一次 commit 都一定会使得镜像变大。

更好的方式,是使用 Dockerfile 来定制镜像,而不是 commit。

5. 镜像构建

镜像构建需要引入 Dockerfile 文件,Dockerfile 是一个文本文件,包含了一条条的指令(instruction),一条指令构建一层,因此每一层指令的内容,就是描述该层如何构建。 有了 Dockerfile 之后使用 docker build 指令来构建镜像。 build 会自动选择当前目录下名为 Dockerfile 的文件,也可以通过 -f 选项来指定 Dockerfile。 build 不光需要 Dockerfile,还需要指定构建的上下文(context)目录,对于上下文的理解,可以参考注意事项中的说明。

构建时通过 -t 选项指定镜像仓库和标签,一个典型的例子为:

docker build -t jerryzhang/app:v1.0 .

表示镜像仓库和名称为 jerryzhang/app ,标签为 v1.0 ,上下文为当前目录。

5.1. Dockerfile 格式

Dockerfile 每一行由指令和参数两部分组成: INSTRUCTION arguments ,指令不缺分大小写,但一般用大写。常用的指令即说明如下:

  • FROM 指定基础镜像,FROM 是 Dockerfile 的第一条指令,也是必备的指令; scratch 是一个特殊的镜像,表示一个空白镜像,不以任何系统为基础,直接可将执行文件复制进镜像(不常见),使用 Go 语言开发通常会使用这种方式来制作镜像。
  • RUN 执行命令,一般包括两种形式:
    • RUN <CMD> Shell 格式,默认是 /bin/sh -c 来执行
    • RUN ["exe", "param1", "param2"] exec 格式
  • COPY 复制文件,将构建上下文目录中的源路径文件拷贝到新一层镜像的目标路径中
  • ADDCOPY 的功能基本一致,但是增加一些额外的功能
    • 源路径可以是一个 URL ,如果是 URL ,Docker 引擎会试图去下载这个链接的文件到目标路径,然后把权限设置为 600 ;如果是压缩包,需要额外解压,比较麻烦,不如使用 RUN 命令,但如果是 tar 压缩文件,格式为 gzip~,~bzip2 以及 xz 的情况下, ADD 会自动解压到目标路径去
    • 根据 Dockerfile 最佳实践文档的要求,尽可能的使用 COPY 来拷贝文件。只有需要自动解压这种需求,才使用 ADD
  • CMDRUN 类似,也可以有 shellexec 两种格式,使用 ENTRYPOINT 指令之后, CMD 指定具体的参数。
    • CMD 用于指定容器主进程的启动命令
    • 运行时,可以指定新的命令来代替镜像设置中的默认命令,比如 docker run -it --rm ubuntu cat /etc/os-release 就是使用 cat /etc/os-release 来代替默认的 /bin/bash 命令
    • 一般推荐使用 exec 格式(会被解析为 JSON 数组,因此一定要使用双引号,而不是单引号)
    • 一个 Dockerfile 中只能有一个 CMD 命令,多个命令是只有最后一个才能生效
  • ENTRYPOINTCMD 指令格式一样,分为 shellexec 两种。而且目的也相同,指定容器的启动程序及参数,一旦指定了 ENTRYPOINT 之后,~CMD~ 的内容将会作为参数传给 ENTRYPOINT 而不是直接运行命令,一般情况下,使用 CMD 即可。 ENTRYPOINT 可以被 docker run 指令中的 -entrypoint 选项指定的指令覆盖(不推荐这么用)。
  • ENV 设置环境变量,环境变量设置之后立即生效,后面的指令可以直接使用,设置方法有:
    • ENV <key> <value>
    • ENV <key1>=<value1> <key2>=<value2>...
  • ARG 构建参数,和 ENV 效果相同,都是设置环境变量,不同的是,ARG 所设置的环境变量只有在构建时期才生效,在容器运行时不存在
  • VOLUME 定义匿名卷,向匿名挂载写入的信息都不会记录进容器存储层,从而保证的容器的无状态化。在运行时,可以通过 -v 参数覆盖掉匿名卷
  • EXPOSE 暴露端口,声明运行时,容器提供服务端口(只是一个声明,运行时并不是因为声明就会开启这个端口的服务),好处是:
    • 帮助镜像使用者理解这个镜像服务的守护端口,方便配置映射
    • 运行时使用随机端口映射时( docker run -P ),会自动随机映射 EXPOSE 端口
    • EXPOSE 容器和 -p 搞混,具体区别看下面的「端口暴露、-p 和 -P 的区别」
  • WORKDIR 指定工作目录,在 Dockerfile 中连续两行命令的执行环境不同(分层存储),像 cd 这样的命令连续两行写将没有任何作用,所以想要改变以后各层的工作目录的位置,应该使用 WORDDIR 命令
  • USER 指定当前用户,和 WORKDIR 相似,改变环境状态并影响以后的层。~USER~ 只是做一个切换用户的操作,用户必须是提前创建好的,否则无法切换
    • 如果以 root 执行的脚本,在执行期间希望改变身份,不要使用 su 或者 sudo ,建议使用 gosu
  • HEALTHCHECK 健康检查,同样支持 shellexec 两种格式。命令的返回值决定了该次健康检查的成功与否,0 成功,1 失败,2 保留(不要使用保留)。告诉 Docker 如何判断容器状态是否正常,指定了 HEATHCHECK 指令之后,启动时状态为 starting ,检查成功状态为 health ,连续一定次数失败,则会变为 unhealthy
    • --interval=<间隔> :两次健康检查的间隔,默认为 30 秒
    • --timeout=<时长> :命令超时时间,超过这个时间,本次健康检查就会被视为失败
    • --retries=<次数> :指定多少次失败之后,容器状态被视为 unhealthy ,默认 3 次
  • LABEL 为镜像添加元数据(标签),是一个 key-value,可用来设置 version、description 等信息,多个 LABEL 数据时,建议合并起来。通过 docker inspect 查看镜像相关的标签信息

因为 Docker 中的每一个指令都会建立一层镜像,所以尽可能把命令合并成一个串联的命令 &&

5.2. 注意事项

  • Docker 在运行时分为 Docker 引擎(服务端的守护进程)和客户端工具,Docker 引擎提供了一组 REST API, docker 命令这样的客户端工具,实际上是通过 API 和 docker 引擎进行交互,从而完成各种功能。构建镜像时, COPY 这种指令并非是在本地构建,而是在服务端,所以在构建镜像时,用户需要提供镜像上下文的路径( docker build 需要指定的目录就是上下文路径),Dockerfile 中指令中的路径不可以超过上下文,否则将无法工作;
  • 容器和虚拟机不同,容器中的应用都应该是以 前台 的方式运行的,容器没有后台的概念。对于容器而言,容器的启动进程就是容器的应用进程,容器就是为了主进程而存在的,主进程退出了,容器就失去了存在意义。所以 CMD service nginx start 容器执行之后就立刻退出了(因为指令运行结束了)。正确的做法是关闭 nginx 的后台运行, CMD ["nginx", "-g", "daemon off;"]
  • 因为 Docker 是在前台运行的,随着内部进程的停止而停止,所以一个 Dockerfile 中必须要指定 CMD 或者 ENTRYPOINT 中的一个(用来启动命令进程);

6. 操作 Docker 容器

6.1. 启动容器

启动容器分为,直接基于镜像新建一个容器 和 将终止状态(stoped)的容器重新启动,将终止状态的容器启动只需要 docker container start 可简写为 docker start

docker run 用于创建并启动容器,一般会使用 -t-i 选项, -t 选项让 Docker 分配一个微终端(pseudo-tty)并绑定到容器的标准输入中, -i 让容器的标准输入保持打开。Docker 后台运行的标准操作包括:

  • 检查本地是否存在指定的镜像,不存在就从公有仓库下载
  • 利用镜像创建并启动一个容器
  • 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层
  • 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去
  • 从地址池配置一个 ip 地址给容器
  • 执行用户指定的应用程序
  • 执行完毕后容器被终止

其它参数:

  • -d 选项可以让 Docker 以后台的方式运行(daemon),输出结果不再会输出到当前宿主机下(可以通过 docker logs 来查看)。

6.2. 终止容器

docker container stop (可简写为 docker stop )用来终止容器运行。终止的容器通过 docker start 可重新启动它。

6.3. 操作容器

6.3.1. 进入容器

后台运行的容器,可通过 attach 或者 exec 命令进入容器操作。推荐使用 exec ,因为从 attach 的容器中退出会导致容器的退出,而 exec 不会。

exec 通常和 -i -t 一起使用,提供交互功能。比如: docker exec -it mongo bash

6.3.2. 查看资源占用

docker stats [OPTIONS] [CONTAINER...]

docker stat 用来实时查看容器资源占用统计, --format 参数用来自定义显示格式, --no-stream 禁止不断刷新,只显示一次。具体看 这里

6.4. 删除容器

docker container rm (可简写为 docker rm )用来删除处于终止状态的容器,如果容器在运行过程中,需要指定 -f 参数,Docker 会发送 SIGKILL 信号给容器。

docker container prune 可清理掉所有处于停止状态的命令。

6.5. 导入和导出容器

  • docker export: 把 docker 容器快照导出到文件中
  • docker import: 把快照文件导入为镜像(注意是镜像,而非容器)

7. 访问仓库

仓库(Repository)是集中存放镜像的,注册服务器(Registry)是管理仓库的具体服务器,一个服务器可以有多个仓库,而仓库可以理解成 Registry 中具体的项目或者目录。

7.1. Docker Hub

Docker Hub 是 Docker 官方维护的公共仓库,涵盖了大部分需求。

  • docker login
  • docker logout
  • docker search: 查找官方仓库中的镜像。搜索结果可以分为两类,一类是类似 centos 的镜像,通常被称为基础镜像或者根镜像。由 Docker 公司创建、验证、支持、提供。另外一种类似 ansible/centos7-ansible 是由 Docker 用户创建并维护的,带有用户名称前缀。查找的时候 --filter=stars=N 可以指定显示收藏数量为 N 以上的镜像。
  • docker pull: 下载镜像到本地
  • docker push: 推送镜像到 Docker Hub。

7.2. 私有仓库

Docker 官方提供了 registry 用于构建私有的镜像仓库。

8. 数据管理

容器中管理数据主要又两种方式:数据卷(Volumes)和挂载主机目录(Bind mounts),但其实挂载的数据目录也是数据卷的一种方式。

8.1. 数据卷

数据卷是一个或多个容器使用的特殊目录,它绕过 UFS,可以提供很多有用的特性:

  • 允许在容器之间共享和重用
  • 对数据卷的修改立马生效
  • 对数据卷的更新,不会影响镜像
  • 生命周期不跟随容器,容器删除了卷依然存在

常用命令:

  • docker volume create my-col: 用于创建数据卷
  • docker volume rm my-col: 删除数据卷
  • docker volume ls: 查看所有的数据卷
  • docker volume inspect my-vol: 查看数据卷的信息
  • docker inspect container: 查看容器的卷信息,数据卷信息在 Mounts key 下面

docker run 使用 --mount 标记来将数据卷挂载到容器里,一次可以挂载多个卷。

docker run -d -P \
    --name web \
    # -v my-vol:/wepapp \
    --mount source=my-vol,target=/webapp \
    training/webapp \
    python app.py

--mountsource target 的方式,可以简单的使用 -v source:target 的方式来实现。

注意:卷的生命周期和容器是独立的,删除容器不会影响挂载卷,如果需要删除容器的同时移除卷,可以在删除容器的时候使用 docker rm -v 命令。同样无主的数据卷可能占用很多空间,使用 docker volume prune 清理。

8.2. 挂载主机目录

Docker 允许挂载一个主机目录作为数据卷。区别仅在于 source 为主机目录,而不是通过 docker volume create 创建的卷。还允许将主机的一个文件挂载到容器中,方式相同,只不过 sourcetarget 都是文件。

9. 容器网络

9.1. 外部访问容器

-p 或者 -P 参数用来设置对外暴露端口。使用 -P 选项,Docker 会随机映射一个 49000~49900 的端口到内部容器开放的网络端口。 -p 可以设置本地端口和容器端口的映射规则( -p 5000:5000 ),如果使用两个冒号 :: 会将绑定任意端口到容器的端口( -p 127.0.0.1::5000 ),本地则会自动分配。而且 -p 允许使用多次绑定多个端口。

  • docker container ls 或者 docker ps -a 可以看到容器对外暴露的端口。
  • docker port container 查看当前映射的端口配置

9.2. 容器互联

推荐使用自定义 Docker 网络来连接多个容器,而不是 --link 参数。

  • docker network create -d bridge my-net: 创建一个新的容器网络
    • -d 参数指定 Docker 网络类型,有 bridgeoverlay ,overlay 用于 Swarm mode
  • docker run --network my-net: 运行容器并连接到新的网络,属于同一个网络下的容器是互通的, ping docker-name

9.3. 配置 DNS

在容器中使用 mount 可以看到挂载信息:

/dev/sda1 on /etc/resolv.conf type ext4 (rw,relatime,data=ordered)
/dev/sda1 on /etc/hostname type ext4 (rw,relatime,data=ordered)
/dev/sda1 on /etc/hosts type ext4 (rw,relatime,data=ordered)

宿主机的 DNS 信息发生变化之后,所有容器的 DNS 配置通过配置 /etc/resolv.conf 文件立刻得到更新。配置全部容器的 DNS,也可以在 /etc/docker/daemon.json 文件中增加以下内容来设置(注:MacOS 下配置在 Preferences > Daemon > Advanced ,而不是配置文件)。

{
  "dns" : [
    "114.114.114.114",
    "8.8.8.8"
  ]
}

除此之外,也可以在 docker run 中手动指定容器的配置:

  • -h HOSTNAME 或者 --hostname=HOSTNAME: 设置容器的主机名,会被写到容器内的 /etc/hostname/etc/hosts ,但它在容器外部看不到,既不会在 docker container ls 中显示,也不会被其它容器的 /etc/hosts 看到
  • --dns=IP_ADDRESS 添加 DNS 服务器到容器的 /etc/resolv.conf ,让容器用这个服务器来解析所有不在 /etc/hosts 中的主机名
  • --dns=DOMAIN 设计容器的搜索域,当设定搜索域为 .example.com 时,在搜索名为 host 的主机时,DNS 不仅搜索 host,还会搜索 host.example.com

10. 常用的镜像库

10.1. BusyBox

BusyBox 是一个很小的镜像库(几兆),集成了一百多个常用的 Linux 命令和工具,方便做快速验证和熟悉 Linux 命令,也可以用来学习 Docker 命令。

10.2. Apline Linux

Apline Linux 是基于 musi libcbusybox 的安全至上的轻量级的 Linux 发行版,而且它提供了自己的包管理工具 apk。Docker 官方已经建议用 Apline 替代之前的 Ubuntu 做为基础镜像环境。

如果要使用 Apline 镜像替换 Ubuntu ,安装软件包时需要用 apk 包管理工具替换 apt。如: apk add --no-cache <package>

11. 实践

不要把 Docker 当成虚拟机来用,Docker 不是虚拟机!

11.1. ENTRYPOINT 和 CMD 的区别

  1. Docker 默认的 ENTRYPOINT 是 /bin/sh -c ,但是没有默认的 CMD。 CMD 是 ENTRYPOINT 的参数 ,一个 Dockerfile 只能有一个 CMD 命令,多个命令只会执行最后一个
  2. 镜像运行时,ENTRYPOINT 命令会最先执行; docker run 后执行的命令会追加到 ENTRYPOINT 之后。ENTRYPOINT 会可以指定脚本:

    COPY ./docker-entrypoint.sh /
    ENTRYPOINT ["/docker-entrypoint.sh"]
    CMD ["postgres"]
    
  3. 在运行 docker 时,添加命令会覆盖 CMD, docker run rails_app rails console 会覆盖默认的命令

使用上的最佳实践

不建议 ENTRYPOINTCMD 一起使用,会增加复杂性。通过只需要 CMD ,只有当你需要吧容器当成一个进程来使用才使用 ENTRYPOINT

ENTRYPOINT 设置镜像的主要命令, CMD 做为默认 flag,比如:

FROM ubuntu
ENTRYPOINT ["top", "-b"]
CMD ["-c"]

语法最佳实践

CMD 应该使用这种格式: CMD ["executable", "param1", "param2", ...]

总结

ENTRYPOINT 和 CMD 都是用来定义容器启动时所要执行的命令,下面说明两者之间的关系:

  1. Dockerfile 应该至少指定一个 CMD 或者 ENTRYPOINT 命令
  2. 当容器做为可执行文件时,应该定义 ENTRYPOINT
  3. CMD 应该做为 ENTRYPOINT 的默认参数,或在容器中执行 ad-hoc 命令
  4. 运行容器设置交互参数(alternative arguments)会覆盖 CMD

via: Docker ENTRYPOINT & CMD: Dockerfile best practices:ENTRYPOINT & CMD

11.2. 端口暴露、-p 和 -P 的区别

有很多资料介绍 EXPOSE-p 的区别,比如 这个 还有 这个,但使用中只要注意:

  • EXPOSE 只是设置暴露端口,主机无法访问,只是一个声明(虽然也可以在 run--expose ,但是写在 Dockerfile 中比较好),通常用于写 Dockerfile 的人设定的服务暴露的端口,类似一个规范性的东西,

一般和 -P 选项一起使用,在命令行指定 -P 之后 EXPOSE 的端口会被随机映射到宿主机上,宿主机上的端口是动态分配的(保证不冲突)。 还因为在构建镜像的时候,你无法决定运行期主机端口如何映射到容器内部,因为你不知道运行容器的机器有哪些端口是可用的。

  • -p 或者 --publish 是实际建立网络映射的方法,创建宿主机和容器的端口映射规则,比如 -p 127.0.0.1:6379:6379 会将容器中的 6379 映射到宿主机的 127.0.0.1:6379

再简单来讲, EXPOSE 只是规范(documenting), -p 才是实际和网络映射有关的。

11.3. Docker 如何跟随系统启动?

启动时添加 --restart=always 参数。

11.4. 对已有的容器(重新)添加端口映射

  1. stop 已运行的容器
  2. commit old to new
  3. run new 设置端口映射

来自:How do I assign a port mapping to an existing Docker container?

11.5. ADD 和 COPY 的区别

都是从上下文环境拷贝数据到镜像中,区别就是 ADDCOPY 做的事情更多一些。

  • ADD 允许 <src> 是个 URL
  • 如果 <src> 是个压缩格式的文件,~ADD~ 也会自动解压。

根据 Dockerfile 最佳实践 中说明,建议使用 COPY 而不是 ADD ,除非你必须要使用 ADD 的特性才能完成工作。

12. FAQ

12.1. 如何获取机器容器 IP

docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' container_name_or_id

12.2. docker 作为 crontab 命令时报错, Error The input device is not a TTY

解决办法时去掉 docker 命令中的 -t 选项,参考:

13. 参考资料

Footnotes:

First created: 2018-08-20 17:45:11
Last updated: 2023-01-01 Sun 17:09
Power by Emacs 29.0.91 (Org mode 9.6.6)