[toc]
Docker 是一款开源软件,用于开发、交付和运行应用程序。 Docker 允许用户将基础设施(Infrastructure)中的应用单独分割出来,形成更小的颗粒(容器),从而提高交付软件的速度。
Docker 容器与虚拟机类似,但二者在原理上不同。容器是将操作系统层虚拟化,虚拟机则是虚拟化硬件,因此容器更具有便携性、高效地利用服务器。 容器更多的用于表示软件的一个标准化单元。由于容器的标准化,因此它可以无视基础设施(Infrastructure)的差异,部署到任何一个地方。
容器相对虚拟机的优势:
- 启动快,容器里面的应用,就是底层系统的一个进程,而不是虚拟机内部的进程。启动容器相当于启动本机的一个进程,而不是启动一个操作系统,所以速度快很多。
- 资源占用少,容器只占用需要的资源,不占用那些没有用到的资源;虚拟机由于是完整的操作系统,不可避免要占用所有资源。另外,多个容器可以共享资源,虚拟机都是独享资源。
- 体积小,容器只要包含用到的组件即可,而虚拟机是整个操作系统的打包,所以容器文件比虚拟机文件要小很多。
Docker 的用途:
提供一次性的环境,比如,本地测试他人的软件、持续集成的时候提供单元测试和构建的环境。
提供弹性的云服务,因为 Docker 容器可以随开随关,很适合动态扩容和缩容。
组建微服务架构,通过多个容器,一台机器可以跑多个服务,因此在本机就可以模拟出微服务架构。
架构
Docker 使用 CS 架构。Client(docker cli) 底层通过 Docker REST API 与 Server(dockerd) 进行通信,并且 Client 和 Server 即可以在同一机器或者不同机器上运行。
Registry 用于存储 Docker image,Docker hub 就是一个公共的 registry,相当于 image 的 github,registry 也可以用来搭建公司或者私人的仓库。
安装和入门
安装
Docker 的安装可以参考官方文档,三大平台都可以安装,甚至树莓派的也有相应的版本可以安装。
需要说明的是,Docker 有几款不同的产品,不同产品的组件功能不同或者拥有的组件不一样。
- Docker Engine,是 Docker 的核心产品,其包含了 docker 和 dockerd。
- Docker Compose,用来运行多容器组合的应用,以 docker-compose.yml 作为启动输入。
- Docker Desktop,Mac 和 Windows 平台的应用,包含了 Docker Engine, Docker CLI client, Docker Compose, Notary, Kubernetes, and Credential Helper 组件。
Mac 如果使用 homebrew 安装的话,要注意 brew install docker
只是安装了 Docker Engine,brew install --cask docker
才是安装 Docker Desktop。当然,也可以直接在这里下载 dmg 文件进行安装。
入门
官方的入门教程写的非常好,跟着官方的教程就可以入门 docker 了,如果只是日常的简单使用或者只是想玩一玩 docker,那这个入门教程就足够了。
这里对官方入门教程做一个简单整理。
启动一个容器
启动容器时,如果对应的 image 不在本地,docker 会去 docker hub 进行搜索,如果 docker hub 上有该 image,则将其拉取到本地并从该 image 启动新的容器。容器与镜像,类似 oop 中的实例与类的差别。实际上,容器就是在不可变的镜像上加了一个可写层。
1 2 3 4 5
# docker run [options] image-name/id # -d detached mode (后台运行模式) # -p 端口映射,[host port]:[containner port] # docker/getting-started 就是官方的入门教程,本地没有的话会从 docker hub 拉取 docker run -dp 80:80 docker/getting-started
创建 image 并打 tag
镜像的 tag,大多数情况下可以理解为镜像的版本,但有时也可以用作一个镜像的别名。同一个镜像可以有多个 tag,就像指针一样,多个指针指向同一块内存地址。
docker build
命令以一个 Dockerfile 文件和一个上下文环境作为基础,进行镜像的创建。Dockerfile 文件是一个遵循 Dockerfile 规范 的文本文件,一般直接用 Dockerfile 作为文件名,当然也可以使用其他文件名,但当使用其他文件名时需要用-f
参数显示指定 Dockerfile 文件。上下文环境就是传给 build 命令的路径或者 url 下的所有文件。1 2 3 4
# 在当前目录下构建 tag 为 getting-started 的 image # 没有显式指定 Dockerfile 时,默认在提供的路径下查找名为 Dockerfile 的文件 # -t 指定 tag docker build -t getting-started .
停止和删除容器
没有命令行的应用场景的话,其实 Mac 和 Windows 的 Docker desktop dashboard 操作起来会更直观和方便,界面上可以直接查看拥有那些 image 以及容器的情况,点点鼠标就可以启动、删减。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
# 查看正在运行的容器 docker ps # 查看所有容器,包括已经停止的,这里需要知道的是,每次 docker run 其实都会新建一个容器并运行,所以如果多次 run 同一个 image,是会产生多个容器的 docker ps -a # 启动一个容器,但是在运行结束后就删除该容器 docker run --rm image-name/id # 停止一个容器 docker stop <the-container-id> # 删除一个容器 docker rm <the-container-id> # 停止并删除一个容器 docker rm -f <the-container-id>
推送到 registy
当本地构建好 image 之后,可以通过推送到 registry 供他人使用或者继续开发。推送前需要登陆到公共或者私有的 registry。
1 2 3 4 5
# 使用 docker hub 时,不需要指定 server,但如果是私人或者公司的 registry,需要显示指定 server 地址 docker login [OPTIONS] [SERVER] # 推送。没有指定具体 tag 时,会推送 tag 为 latest 的 iamge docker push xxx/xxxxxx:tag
持久化
持久化就是把容器运行中的一些数据保存在 host 上,以便其他容器或者应用使用。有两种实现持久化的方式:一种是 Named Volume;另一种是 Bind Mount。
Named Voulme:通过
docker volume
创建一个有名称的 volume,在docker run
时再指定相应的 volume即可。1 2 3 4 5 6 7 8
# 创建一个名为 todo-db 的 volume docker volume create todo-db # 查看 todo-db volume的详细信息,譬如在磁盘上的实际存储位置 docker volume inspect todo-db # -v 指定容器运行时要使用的 volume,[volume name]:[container mount point] docker run -dp 3000:3000 -v todo-db:/etc/todos getting-started
Bind Mount:不显式创建 volume,直接绑定 host 的具体目录到容器的具体目录。
1 2 3 4 5
# 把 host 当前目录挂载到容器的 /app 文件夹 docker run -dp 3000:3000 \ -w /app -v "$(pwd):/app" \ node:12-alpine \ sh -c "yarn install && yarn run dev"
多容器应用
Docker 容器的理念是,一个容器做好一件事。那么比如遇到一个需要数据库的 web 应用时,最少就需要三个容器,前后端和数据库各一个容器。这种多容器类型的应用,Docker 有两种解决方式:一种是通过创建网络,将多容器连接到同一网络,分别启动;另一种是通过 compose,由 Docker 来管理多应用,开发人员只提供相应的配置文件(compose.yaml),在配置文件里指定各容器和其之间的联系。
容器网络:各容器默认情况下是不知道其他容器存在的,需要通过网络来让它们联系在一起。网络的创建与 volume 类似。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
# 创建一个名为 todo-app 的网络 docker network create todo-app # 启动 mysql 容器,--network 指定容器运行的网络,--netword-alias 指定容器在网络中的别名 # -e 提供环境变量 docker run -d \ --network todo-app --network-alias mysql \ -v todo-mysql-data:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORD=secret \ -e MYSQL_DATABASE=todos \ mysql:5.7 # 启动 app 容器,--network 指定容器运行网络,-e MYSQL_HOST 指定 mysql 的地址为之前启动的 mysql 容器网络别名 docker run -dp 3000:3000 \ -w /app -v "$(pwd):/app" \ --network todo-app \ -e MYSQL_HOST=mysql \ -e MYSQL_USER=root \ -e MYSQL_PASSWORD=secret \ -e MYSQL_DB=todos \ node:12-alpine \ sh -c "yarn install && yarn run dev"
Docker Compsoe:Docker 官方出品的用于定义和分享多容器应用的工具,利用该工具只需要创建一个 yaml 文件就可以实现一条命令启停多容器应用。Yaml 文件需要遵循 Compose File 规范。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
# schema version version: "3.7" # 声明和定义所有容器 services: # 定义一个名为 app 的容器 app: # 要从哪个 image 启动容器 image: node:12-alpine # 启动后执行的命令 command: sh -c "yarn install && yarn run dev" # 端口映射 ports: - 3000:3000 # 指定工作目录 working_dir: /app # 指定 bind mount volumes: - ./:/app # 环境变量 environment: MYSQL_HOST: mysql MYSQL_USER: root MYSQL_PASSWORD: secret MYSQL_DB: todos # 定义一个名为 mysql 的容器 mysql: image: mysql:5.7 volumes: - todo-mysql-data:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: secret MYSQL_DATABASE: todos # 声明和定义 volume volumes: todo-mysql-data:
1 2 3 4 5 6 7 8 9
# 在后台启动多容器应用 docker-compose up -d # 查看日志 docker-compose logs -f docker-compose logs -f app # 停止多容器应用,注意如果要删除 volume,还需要加上 --volumes 标志 docker-compose down
其他
1 2 3 4 5
# 查看一个镜像的 layer,--no-trunc 表示显示所有内容 docker image history image # 查看 docker 对象的底层信息 docker inspect xxx
CMD 和 ENTRYPOINT
Dockerfile 中的 RUN、CMD、ENTRYPOINT 都可以用来执行命令。RUN 比较清晰,多用来安装一些软件,创建一个新的镜像层。CMD 和 ENTRYPOINT 比较绕,这里做个梳理。
CMD 和 ENTRYPOINT 指令都有 exec 和 shell 两种格式,其中 exec 是官方的推荐格式,CMD 额外多一种默认参数的格式。
1
2
3
4
5
6
7
8
# exec 格式,executable 就是可执行程序,其余是参数。这是官方的推荐格式。
CMD/ENTRYPOINT ["executable","param1","param2"]
# shell 格式
CMD/ENTRYPOINT command param1 param2
# 默认参数格式
CMD ["param1","param2"]
CMD
CMD 指令允许用户指定容器启动后的默认执行命令。
当容器没有指定 entrypoint 并且 docker run 没有指定其他命令时,CMD 的 exec 和 shell 格式的指令会被执行。譬如:
1
2
3
4
5
6
7
8
9
10
11
12
# 在 Dockerfile 中指定 echo 作为容器启动后的默认命令
CMD ["/bin/echo", "Hello world"]
# 当容器启动时未指定命令,则会执行 echo 命令并输出 Hello world
docker run -it [image]
Hello world
# 当容器启动时指定了命令,则启动后会执行指定的命令,CMD 指定的默认 echo 命令将被忽略
docker run -it [image] /bin/ls
bin
boot
...
ENTRYPOINT
ENTRYPOINT 用于设置容器启动时要执行的命令及其参数,同时可通过 CMD 命令或者命令行参数提供额外的参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 指定容器启动时执行的命令和参数
ENTRYPOINT ["/bin/echo", "Hello"]
# 容器无参数启动时
docker run -it [image]
Hello
# 容器带参数启动时,会追加参数,可以等价于 ENTRYPOINT ["/bin/echo", "Hello", "world"]
docker run -it [image] world
Hello world
# Dockerfile 中同时指定了 ENTRYPOINT 和 CMD 的默认参数形式,CMD 会作为 ENTRYPOINT 的默认参数
ENTRYPOINT ["/bin/echo", "Hello"]
CMD ["world"]
# 容器无参数启动时
docker run -it [image]
Hello world
# 容器带参数启动时,CMD 的默认参数被覆盖
docker run -it [image] China
Hello China
Dockerfile 中的 ENTRYPOINT 可以被 –entrypoint 覆盖。
You can override the
ENTRYPOINT
setting using--entrypoint
, but this can only set the binary to exec (nosh -c
will be used).
Combination of CMD & ENTRYPOINT
贴上官网的 CMD 和 ENTRYPOINT 组合表格,还是非常清楚的。
No ENTRYPOINT | ENTRYPOINT exec_entry p1_entry | ENTRYPOINT [“exec_entry”, “p1_entry”] | |
---|---|---|---|
No CMD | error, not allowed | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry |
CMD [“exec_cmd”, “p1_cmd”] | exec_cmd p1_cmd | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry exec_cmd p1_cmd |
CMD [“p1_cmd”, “p2_cmd”] | p1_cmd p2_cmd | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry p1_cmd p2_cmd |
CMD exec_cmd p1_cmd | /bin/sh -c exec_cmd p1_cmd | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd |
Java
通过 Jib 可以很方便的容器化 Java 应用。Jib 是一款 maven 插件,使用该插件不需要有容器 runtime,只需要了解 mvn 指令就可以完成 image 的构建。
下边是一个 Hello World
的示例。
通过
mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4
生成一个项目。在 pom.xml 加入jib-maven-plugin
依赖,并通过<from> <image>
标签指定 base-image,通过<to> <image>
指定生成的 image:tag。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
<plugin> <groupId>com.google.cloud.tools</groupId> <artifactId>jib-maven-plugin</artifactId> <version>3.0.0</version> <configuration> <from> <!-- base image --> <image>openjdk:alpine</image> </from> <to> <!-- 生成的 image name 和 tage --> <image>jib-demo:1.0-SNAPSHOT</image> </to> </configuration> <container> <!-- 指定生成的 image 创建时间使用当前时间戳 --> <creationTime>USE_CURRENT_TIMESTAMP</creationTime> </container> </plugin>
然后执行
mvn clean compile jib:dockerBuild
就可以生成 image 到本地 docker daemon 了,前提是 docker daemon 要在运行状态。前边提到 jib 这个插件是不需要容器运行时的,如果不是进行本地调试想直接发布的话,则可以通过mvn clean compile jib:build
命令,这个会直接发布 image 到 registry,至于是推送到公共的 docker hub 还是私有仓库取决于<to> <image>
的配置,具体可以参考官方文档。最后检查本地的 image,发现已经有
jib-demo:1.0-SNAPSHOT
镜像了,直接运行就会打印出 Hello World。1 2 3 4 5 6
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE jib-demo 1.0-SNAPSHOT 9e1a1730697e 9 seconds ago 103MB $ docker run jib-demo:1.0-SNAPSHOT Hello World!
关于 jib 还有几点需要提及的。
生成 image 还有第三种方式:
mvn compile jib:buildTar
,这会生成一个本地的 tarball,可以复制、拷贝到其他机器或者在本机,再载入:docker load --input target/jib-image.tar
如果不指定 base image,jib 默认会使用 AdoptOpenJDK 作为 base image
文件结构:默认情况下生成的 image 中会有三个文件夹存放 java 相关的内容,app/classes 下是项目的所有类文件;app/libs 是项目的所有依赖;app/resources 是项目所有的资源文件。通过
docker inspect
查看生成的 image,会发现其 entrypoint 是把这个三个文件夹指定为了 classpath。1 2 3 4 5 6
"Entrypoint": [ "java", "-cp", "/app/resources:/app/classes:/app/libs/*", "com.cy.demo.App" ],
如果配置了
containerizingMode
为packaged
,那么 jib 会把 target 下的 jar 包拷贝到 app/classpath 目录下,同时 entrypoint 会变为以下这样。1 2 3 4 5 6
"Entrypoint": [ "java", "-cp", "/app/classpath/*:/app/libs/*", "com.cy.demo.App" ],
常用命令整理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# 运行,-p 端口映射;-it 交互终端;-d 后台运行;--name 指定容器名
docker run
# 查看容器,-a 所有容器;--no-trunc 显示长 id
docker ps
# 从镜像创建容器,但不启动
docker create --myNginx nginx
# 启停删容器
docker start/pause/restart/stop/rm myNginx
# 查看记录
t0=$(date "+%Y-%m-%dT%H:%M:%S")
...
t1=$(date "+%Y-%m-%dT%H:%M:%S")
docker evnets --since $t0 --until $t1
# 查看详细信息
docker inspect
# 查看资源使用情况
docker stats
# 查看容器内进程
docker top myNginx
# 查看网络,bridge 是默认网络,容器启动后会连接到这个子网,通过 inspect 可以查看
docker network ls
docker netword inspect bridge
# 创建网络,--driver 指定网络类型
docker network create --driver bridge myNetwork
# 指定容器运行的网络 --net
docker run --net=myNetwork myNginx
# 把运行中的容器连接到网络,这样默认的 bridge 和 myNetwork 两个子网中都会有该容器
docker network connect/diconnect myNginx myNetwork
# 查看日志
docker logs myNginx
# 在一个运行的容器中执行命令,exec 和 attach。
# exec 是重新启动一个 shell 进程,它退出后不会影响原先运行的容器
# attach 顾名思义是附到了原进程上,它退出后也会停止容器的运行
docker attach myNginx
docker exec myNginx ls /
docker exec -it myNginx /bin/bashs
# 提供环境变量
docker run --env MYVAR1=foo nginx
docker run --env-file ./env-file nginx
# 查看镜像
docker images
# 拉取镜像,不指定 tag 时,默认 latest
docker pull hello-world
docker pull hello-world:linux
# 登陆,不指定地址时,默认为 docker hub
docker login
# 创建镜像
docker build --tag "hello-world:1.0-SNAPSHOT" .
# 创建 volume
docker volume create myVolume
docker volume ls
docker volume inspect myVolume
# 挂载 volume
docker run -it --mount source=myVolume,target=/app alpine