# Docker 实践

# 一、安装 Docker

Ubuntu 安装 Docker CE (opens new window)

macOS 安装 Docker (opens new window)

# 二、 Docker 架构与核心概念

Docker 使用 client-server 架构, Docker 客户端将命令发送给 Docker 守护进程,后者负责构建,运行和分发 Docker 容器。 Docker 客户端和守护程序使用 REST API,通过 UNIX 套接字或网络接口进行通信。核心概念如下:

# 2.1 镜像

Docker 镜像(Image)是一个特殊的文件系统,包含了程序运行时候所需要的资源和环境。镜像不包含任何动态数据,其内容在构建之后也不会被改变。

因为镜像包含操作系统完整的 root 文件系统,其体积往往是庞大的,因此在 Docker 设计时,充分利用 Union FS (联合文件系统)的技术,将其设计为分层存储的架构,所以一个镜像实际上是由多层文件系统联合组成。镜像构建时,会一层层构建,前一层是后一层的基础;每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。因此,在构建镜像的时候,需要额外小心,每一层尽量只包含该层需要添加的东西,任何额外的东西应该在该层构建结束前清理掉。

分层存储的特征使得镜像的复用、定制变的更为容易。甚至可以用之前构建好的镜像作为基础层,然后进一步添加新的层,以定制自己所需的内容,构建新的镜像。

# 2.2 容器

镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的 实例 一样,镜像是静态的定义,容器是镜像运行时的实体,容器可以被创建、启动、停止、删除、暂停等。

容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行在属于自己的、独立的命名空间中。因此容器可以拥有自己的 root 文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样,这种特性使得容器封装的应用比直接在宿主运行更加安全。

前面讲过镜像使用的是分层存储,容器也是如此。每一个容器运行时,是以镜像为基础层,在其上创建一个当前容器的存储层,我们可以称这个为容器运行时读写而准备的存储层称为 容器存储层。容器存储层的生存周期和容器一样,容器消亡时,容器存储层也随之消亡。因此,任何保存于容器存储层的信息都会随容器删除而丢失。

按照 Docker 最佳实践的要求,容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化。所有的文件写入操作,都应该使用数据卷(Volume)、或者绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。数据卷的生存周期独立于容器,容器消亡,数据卷不会消亡,因此,使用数据卷后,容器删除或者重新运行之后,数据都不会丢失。

# 2.3 仓库

镜像构建完成后,可以很容易的在当前宿主机上运行,但如果需要在其它服务器上使用这个镜像,就需要一个集中的存储、分发镜像的服务,这就是镜像仓库(Registry)。Docker Hub (opens new window) 是 Docker 官方提供的镜像公有仓库,提供了大量常用软件的镜像,当然出于安全和保密的需要,你也可以构建自己的私有仓库。

# 2.4 Docker daemon

Docker daemon(dockerd)负责监听 Docker API 请求并管理 Docker 对象,如镜像,容器,网络和卷,守护程序彼此之间也可以进行通讯。

# 2.5 Docker Client

Docker 客户端(docker)是用户与 Docker 交互的主要方式。当你使用 docker run 等命令时,客户端会将这些命令发送到 dockerd,dockerd 负责将其执行。一个 Docker客户端可以与多个 dockerd 进行通讯。

# 三、镜像相关命令

# 3.1 获取镜像

Docker Hub (opens new window) 上有大量的高质量的镜像可以用。从 Docker 镜像仓库获取镜像的命令是 docker pull。其命令格式为:

docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]
1
  • Docker 镜像仓库地址:地址的格式一般是 <域名/IP>[:端口号]。默认地址是 Docker Hub。
  • 仓库名:如之前所说,这里的仓库名是两段式名称,即 <用户名>/<软件名>。对于 Docker Hub,如果不给出用户名,则默认为 library,也就是官方镜像。

示例:

docker pull ubuntu:18.04
1

# 运行

有了镜像后,我们就能够以这个镜像为基础启动并运行一个容器。以上面的 ubuntu:18.04 为例,如果我们打算启动里面的 bash 并且进行交互式操作的话,可以执行下面的命令。

$ docker run -it --rm \
    ubuntu:18.04 \
    bash
1
2
3

docker run 就是运行容器的命令,具体格式我们会在 容器 一节进行详细讲解,我们这里简要的说明一下上面用到的参数。

  • -it:这是两个参数,一个是 -i:交互式操作,一个是 -t 终端。我们这里打算进入 `bash`` 执行一些命令并查看返回结果,因此我们需要交互式终端。
  • --rm:这个参数是说容器退出后随之将其删除。
  • ubuntu:18.04:这是指用 ubuntu:18.04 镜像为基础来启动容器。
  • bash:放在镜像名后的是 命令,这里我们希望有个交互式 Shell,因此用的是 bash

# 3.2 列出镜像

列出已经下载下来的镜像

docker image ls
1

查看镜像、容器、数据卷所占用的空间 docker system df

列出特定的某个镜像 docker image ls ubuntu:18.04

看到在 mongo:3.2 之后建立的镜像 docker image ls -f since=mongo:3.2

想查看某个位置之前的镜像也可以,只需要把 since 换成 before 即可。

此外,如果镜像构建时,定义了 LABEL,还可以通过 LABEL 来过滤。

$ docker image ls -f label=com.example.version=0.1
1

直接列出镜像结果,并且只包含镜像ID和仓库名

$ docker image ls --format "{{.ID}}: {{.Repository}}"
1

查看容器的存储层变动

docker diff
1

查看镜像内的历史记录

docker history nginx:v2
1

# 3.3 删除镜像

# 语法
docker image rm [选项] <镜像1> [<镜像2> ...]
# 删除虚悬镜像
docker image prune
docker image prune --force --all
# 删除所有镜像
docker image rm $(docker image ls --format "{{.ID}}")
# 删除所有仓库名为 redis 的镜像
docker image rm $(docker image ls -q redis)
# 或者删除所有在 mongo:3.2 之前的镜像
docker image rm $(docker image ls -q -f before=mongo:3.2)
1
2
3
4
5
6
7
8
9
10
11

# 3.4 创建镜像

# Dockerfile

FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
1
2

# 创建镜像

# 注意,最后一个点是 docker 的打包目录, docker 将该目录下所有的文件都打包进镜像。
docker build -t nginx:v3 -f nginx-Dockerfile .
1
2

# 3.5 其他命令

# 查看镜像支持的环境变量
docker run IMAGE env
1
2

# 四、容器相关命令

# 4.1 查看容器

# 查看正在运行的容器
docker ps
# 查看所有容器
docker ps -a
# 如何获取某个容器的 PID 信息
docker inspect --format '{{ .State.Pid }}' <CONTAINER ID or NAME>
何获取某个容器的 IP 地址
docker inspect --format '{{ .NetworkSettings.IPAddress }}' <CONTAINER ID or NAME>
# 列出所有容器ID
docker ps -aq
1
2
3
4
5
6
7
8
9
10

# 4.2 运行容器

# 用 nginx 镜像启动一个容器,命名为 webserver,并且映射了 80 端口
docker run --name webserver -d -p 80:80 nginx

# 交互方式访问运行中的容器
docker exec -it CONTAINER  bash
1
2
3
4
5

# 4.3 停止容器

docker stop 容器名
# 停止所有正在运行的容器
docker stop $(docker container ls -q)

1
2
3
4

# 4.4 删除容器

# 删除所有容器
docker rm $(docker ps -aq)

docker container rm 容器名
# 批量清理已经停止的容器
docker container prune
docker container prune -f

# 删除 exited 状态容器
docker rm $(docker ps --all -q -f status=exited)
1
2
3
4
5
6
7
8
9
10

# 4.5 容器网络设定

# 给容器指定一个固定 IP 地址,而不是每次重启容器 IP 地址都会变
$ docker network create -d bridge --subnet 172.25.0.0/16 my-net
$ docker run --network=my-net --ip=172.25.3.3 -itd --name=my-container busybox
# 临时退出一个正在交互的容器的终端,而不终止它
按 Ctrl-p Ctrl-q。如果按 Ctrl-c 往往会让容器内应用进程终止,进而会终止容器。
1
2
3
4
5

# 4.6 删除网络

docker network ls
docker network rm my_network
1
2

# 五、DockerFile

dockerfile 是 Docker 用来构建镜像的文本文件,包含自定义的指令和格式,可以通过 build 命令从 dockerfile 中构建镜像,命令格式为:docker build [OPTIONS] PATH | URL | -

dockerfile 描述了组装镜像的步骤,其中每条指令都是单独执行的。除了 FROM 指令,其他的每一条指令都会在上一条指令所生成镜像的基础上执行,执行完后会生成一个新的镜像层,新镜像层覆盖在原来的镜像之上从而形成新的镜像。为了提高镜像构建的速度, Docker Daemon 会缓存构建过程中的中间镜像。在构建镜像时,它会将 dockerfile 中下一条指令和基础镜像的所有子镜像做比较,如果有一个子镜像是由相同的指令生成的,则命中缓存,直接使用该镜像,而不用再生成一个新的镜像。常用指令如下:

# 5.1 FROM

FROM 指令用于指定基础镜像,因此所有的 dockerfile 都必须使用 FROM 指令开头。FROM 指令可以出现多次,这样会构建多个镜像,每个镜像创建完成后,Docker 命令行界面会输出该镜像的 ID。常用指令格式为:FROM <image>[:<tag>] [AS <name>]

# 5.2 MAINTAINER

MAINTAINER 指令可以用来设置作者名称和邮箱,目前 MAINTAINER 指令被标识为废弃,官方推荐使用 LABEL 代替。

# 5.3 LABEL

LABEL 指令可以用于指定镜像相关的元数据信息。格式为:LABEL <key>=<value> <key>=<value> <key>=<value> ...

# 5.4 ENV

ENV 指令用于声明环境变量,声明好的环境变量可以在后面的指令中引用,引用格式为 $variable_name${variable_name} 。常用格式有以下两种:

  • ENV <key> <value> :用于设置单个环境变量;
  • ENV <key>=<value> ... :用于一次设置多个环境变量。

# 5.5 EXPOSE

EXPOSE 用于指明容器对外暴露的端口号,格式为:EXPOSE <port> [<port>/<protocol>...] ,您可以指定端口是侦听 TCP 还是 UDP,如果未指定协议,则默认为 TCP。

# 5.6 WORKDIR

WORKDIR 用于指明工作目录,它可以多次使用。如果指明的是相对路径,则它将相对于上一个WORKDIR指令的路径。示例如下:

WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd # 此时pwd为:/a/b/c
1
2
3
4

# 5.7 COPY

COPY 指令的常用格式为:COPY <src>... <dest>,用于将指定路径中的文件添加到新的镜像中,拷贝的目标路径可以不存在,程序会自动创建。

# 5.8 ADD

ADD 指令的常用格式为:COPY <src>... <dest>,作用与 COPY 指令类似,但功能更为强大,例如 Src 支持文件的网络地址,且如果 Src 指向的是压缩文件,ADD 在复制完成后还会自动进行解压。

# 5.9 RUN

RUN 指令会在前一条命令创建出的镜像基础上再创建一个容器,并在容器中运行命令,在命令结束后提交该容器为新的镜像。它支持以下两种格式:

  • RUN <command>shell 格式)
  • RUN ["executable", "param1", "param2"] (exec 格式)

使用 shell 格式时候,命令通过 /bin/sh -c 运行,而当使用 exec 格式时,命令是直接运行的,容器不调用 shell 程序,这意味着不会发生正常的 shell 处理。例如,RUN ["echo","$HOME"] 不会对 $HOME 执行变量替换,此时正确的格式应为:RUN ["sh","-c","echo $HOME"]。下面的 CMD 指令也存在同样的问题。

# 5.10 CMD

  • CMD ["executable","param1","param2"] (exec 格式, 首选)
  • CMD ["param1","param2"] (作为 ENTRYPOINT 的默认参数)
  • CMD command param1 param2 (shell 格式)

CMD 指令提供容器运行时的默认值,这些默认值可以是一条指令,也可以是一些参数。一个 dockerfile 中可以有多条 CMD 指令,但只有最后一条 CMD 指令有效。CMD 指令与 RUN 指令的命令格式相同,但作用不同:RUN 指令是在镜像的构建阶段用于产生新的镜像;而 CMD 指令则是在容器的启动阶段默认将 CMD 指令作为第一条执行的命令,如果用户在 docker run 时指定了新的命令参数,则会覆盖 CMD 指令中的命令。

# 5.11 ENTRYPOINT

ENTRYPOINT 指令 支持以下两种格式:

  • ENTRYPOINT ["executable", "param1", "param2"] (exec 格式,首先)
  • ENTRYPOINT command param1 param2 (shell 格式)

ENTRYPOINT 指令 和 CMD 指令类似,都可以让容器在每次启动时执行相同的命令。但不同的是 CMD 后面可以是参数也可以是命令,而 ENTRYPOINT 只能是命令;另外 docker run 命令提供的运行参数可以覆盖 CMD,但不能覆盖 ENTRYPOINT ,这意味着 ENTRYPOINT 指令上的命令一定会被执行。如下 dockerfile 片段:

ENTRYPOINT ["/bin/echo", "Hello"]
CMD ["world"]
1
2

当执行 docker run -it image 后,此时输出为 hello world,而当你执行 docker run -it image spring ,此时 CMD 中的参数会被覆盖,此时输出为hello spring

# 六、docker-compose

# 启动 docker
docker-compose up

# 停止 docker
docker-compose down

# 启动 docker 并在后台运行
docker-compose up -d

# 重编译 docker 并运行
docker-compose up --build 
1
2
3
4
5
6
7
8
9
10
11

# 备份数据库

docker exec CONTAINER /usr/bin/mysqldump -u root --password=root DATABASE > backup.sql
1

# 恢复数据库

cat backup.sql | docker exec -i CONTAINER /usr/bin/mysql -u root --password=root DATABASE
1
更新时间: 6/25/2021, 1:11:55 PM