Home Docker
Post
Cancel

Docker

[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-arch

安装和入门

安装

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,那这个入门教程就足够了。

Getting started

这里对官方入门教程做一个简单整理。

  1. 启动一个容器

    启动容器时,如果对应的 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
    
  2. 创建 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 .
    
  3. 停止和删除容器

    没有命令行的应用场景的话,其实 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>
    
  4. 推送到 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
    
  5. 持久化

    持久化就是把容器运行中的一些数据保存在 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"
    
  6. 多容器应用

    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
    
  7. 其他

    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 (no sh -c will be used).

Combination of CMD & ENTRYPOINT

贴上官网的 CMD 和 ENTRYPOINT 组合表格,还是非常清楚的。

 No ENTRYPOINTENTRYPOINT exec_entry p1_entryENTRYPOINT [“exec_entry”, “p1_entry”]
No CMDerror, not allowed/bin/sh -c exec_entry p1_entryexec_entry p1_entry
CMD [“exec_cmd”, “p1_cmd”]exec_cmd p1_cmd/bin/sh -c exec_entry p1_entryexec_entry p1_entry exec_cmd p1_cmd
CMD [“p1_cmd”, “p2_cmd”]p1_cmd p2_cmd/bin/sh -c exec_entry p1_entryexec_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_entryexec_entry p1_entry /bin/sh -c exec_cmd p1_cmd

Java

通过 Jib 可以很方便的容器化 Java 应用。Jib 是一款 maven 插件,使用该插件不需要有容器 runtime,只需要了解 mvn 指令就可以完成 image 的构建。

下边是一个 Hello World 的示例。

  1. 通过 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>
    
  2. 然后执行 mvn clean compile jib:dockerBuild 就可以生成 image 到本地 docker daemon 了,前提是 docker daemon 要在运行状态。前边提到 jib 这个插件是不需要容器运行时的,如果不是进行本地调试想直接发布的话,则可以通过 mvn clean compile jib:build 命令,这个会直接发布 image 到 registry,至于是推送到公共的 docker hub 还是私有仓库取决于 <to> <image> 的配置,具体可以参考官方文档

  3. 最后检查本地的 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"
    ],
    

    如果配置了 containerizingModepackaged,那么 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
This post is licensed under CC BY 4.0 by the author.