Docker 筑梦师系列(一):实现容器互联

在实际应用中,不一样的服务之间是须要通讯的,例如后端 API 和数据库;幸运的是,Docker 为咱们提供了网络(Network)机制,可以轻松实现容器互联。这篇文章将带你轻松上手 Docker 网络,学会使用默认网络和自定义网络,成为一名可以链接多个“梦境”的筑梦师!前端

Docker一杯茶教程中,咱们带你了解了镜像和容器这两大关键的概念,熟悉了经常使用的 docker 命令,并成功地容器化了第一个应用。可是,那只是咱们“筑梦之旅”的序章。接下来,咱们将实现后端 API 服务器 + 数据库的容器化。node

若是您以为咱们写得还不错,记得 点赞 + 关注 + 评论 三连,鼓励咱们写出更好的教程💪

咱们为你准备好了应用程序代码,请运行如下命令:linux

# 若是你看了上一篇教程,仓库已经克隆下来了
cd docker-dream
git fetch origin network-start
git checkout network-start

# 若是你打算直接从这篇教程开始
git clone -b network-start https://github.com/tuture-dev/docker-dream.git
cd docker-dream

和以前容器化前端静态页面服务器相比,多了一个难点:服务器和数据库分别是两个独立的容器,可是服务器须要链接和访问数据库,怎么实现跨容器之间的通讯?nginx

在《盗梦空间》中,不一样的梦境之间是没法链接的,然而幸运的是在 Docker 中是能够的——借助 Docker Network。git

提示

在早期,Docker 容器能够经过 docker run 命令的 --link 选项来链接容器,可是 Docker 官方宣布这种方式已通过时,并有可能被移除
参考文档)。而本文将讲解 Docker 官方推荐的方式链接容器:自定义网络(User-defined Networks)。github

Network 类型

Network,顾名思义就是“网络”,可以让不一样的容器之间相互通讯。首先有必要要列举一下 Docker Network 的五种驱动模式(driver):mongodb

  • bridge:默认的驱动模式,即“网桥”,一般用于单机(更准确地说,是单个 Docker 守护进程)
  • overlay:Overlay 网络可以链接多个 Docker 守护进程,一般用于集群,后续讲 Docker Swarm 的文章会重点讲解
  • host:直接使用主机(也就是运行 Docker 的机器)网络,仅适用于 Docker 17.06+ 的集群服务
  • macvlan:Macvlan 网络经过为每一个容器分配一个 MAC 地址,使其可以被显示为一台物理设备,适用于但愿直连到物理网络的应用程序(例如嵌入式系统、物联网等等)
  • none:禁用此容器的全部网络

这篇文章将围绕默认的 Bridge 网络驱动展开。没错,就是链接不一样梦境的那座“桥”。docker

小试牛刀

咱们仍是经过一些小实验来理解和感觉 Bridge Network。与上一节不一样的是,咱们将使用 Alpine Linux 镜像做为实验原材料,由于:shell

  • 很是轻量小巧(整个镜像仅 5MB 左右)
  • 功能丰富,比“瑞士军刀” Busybox 还要完善

网桥网络可分为两类:数据库

  1. 默认网络(Docker 运行时自带,不推荐用于生产环境)
  2. 自定义网络(推荐使用)

让咱们分别实践一下吧。

默认网络

这个小实验的内容以下图所示:

咱们会在默认的 bridge 网络上链接两个容器 alpine1alpine2。 运行如下命令,查看当前已有的网络:

docker network ls

应该会看到如下输出(注意你机器上的 ID 颇有可能不同):

NETWORK ID          NAME                DRIVER              SCOPE
cb33efa4d163        bridge              bridge              local
010deedec029        host                host                local
772a7a450223        none                null                local

这三个默认网络分别对应上面的 bridgehostnone 网络类型。接下来咱们将建立两个容器,分别名为 alpine1alpine2,命令以下:

docker run -dit --name alpine1 alpine
docker run -dit --name alpine2 alpine

-dit-d(后台模式)、-i(交互模式)和 -t(虚拟终端)三个选项的合并。经过这个组合,咱们可让容器保持在后台运行而不会退出(没错,至关因而在“空转”)。

docker ps 命令肯定以上两个容器均在后台运行:

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
501559d2fab7        alpine              "/bin/sh"           2 seconds ago       Up 1 second                             alpine2
18bed3178732        alpine              "/bin/sh"           3 seconds ago       Up 2 seconds                            alpine1

经过如下命令查看默认的 bridge 网络的详情:

docker network inspect bridge

应该会输出 JSON 格式的网络详细数据:

[
  {
    "Name": "bridge",
    "Id": "cb33efa4d163adaa61d6b80c9425979650d27a0974e6d6b5cd89fd743d64a44c",
    "Created": "2020-01-08T07:29:11.102566065Z",
    "Scope": "local",
    "Driver": "bridge",
    "EnableIPv6": false,
    "IPAM": {
      "Driver": "default",
      "Options": null,
      "Config": [
        {
          "Subnet": "172.17.0.0/16",
          "Gateway": "172.17.0.1"
        }
      ]
    },
    "Internal": false,
    "Attachable": false,
    "Ingress": false,
    "ConfigFrom": {
      "Network": ""
    },
    "ConfigOnly": false,
    "Containers": {
      "18bed3178732b5c7a37d7ad820c111fac72a6b0f47844401d60a18690bd37ee5": {
        "Name": "alpine1",
        "EndpointID": "9c7d8ee9cbd017c6bbdfc023397b23a4ce112e4957a0cfa445fd7f19105cc5a6",
        "MacAddress": "02:42:ac:11:00:02",
        "IPv4Address": "172.17.0.2/16",
        "IPv6Address": ""
      },
      "501559d2fab736812c0cf181ed6a0b2ee43ce8116df9efbb747c8443bc665b03": {
        "Name": "alpine2",
        "EndpointID": "da192d61e4b2df039023446830bf477cc5a9a026d32938cb4a350a82fea5b163",
        "MacAddress": "02:42:ac:11:00:03",
        "IPv4Address": "172.17.0.3/16",
        "IPv6Address": ""
      }
    },
    "Options": {
      "com.docker.network.bridge.default_bridge": "true",
      "com.docker.network.bridge.enable_icc": "true",
      "com.docker.network.bridge.enable_ip_masquerade": "true",
      "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
      "com.docker.network.bridge.name": "docker0",
      "com.docker.network.driver.mtu": "1500"
    },
    "Labels": {}
  }
]

咱们重点要关注的是两个字段:

  • IPAM:IP 地址管理信息(IP Address Management),能够看到网关地址为 172.17.0.1(因为篇幅有限,想要了解网关的同窗可自行查阅计算机网络以及 TCP/IP 协议方面的资料)
  • Containers:包括此网络上链接的全部容器,能够看到咱们刚刚建立的 alpine1alpine2,它们的 IP 地址分别为 172.17.0.2172.17.0.3(后面的 /16 是子网掩码,暂时不用考虑)

提示

若是你熟悉 Go 模板语法,能够经过 -fformat)参数过滤掉不须要的信息。例如咱们只想查看 bridge 的网关地址:

$ docker network inspect --format '{{json .IPAM.Config }}' bridge
[{"Subnet":"172.17.0.0/16","Gateway":"172.17.0.1"}]

让咱们进入 alpine1 容器中:

docker attach alpine1
注意

attach 命令只能进入设置了交互式运行的容器(也就是在启动时加了 -i 参数)。

若是你看到前面的命令提示符变成 / #,说明咱们已经身处容器之中了。咱们经过 ping 命令测试一下网络链接状况,首先 ping 一波图雀社区的主站 tuture.co(-c 参数表明发送数据包的数量,这里咱们设为 5):

/ # ping -c 5 tuture.co
PING tuture.co (150.109.19.98): 56 data bytes
64 bytes from 150.109.19.98: seq=2 ttl=37 time=65.294 ms
64 bytes from 150.109.19.98: seq=3 ttl=37 time=65.425 ms
64 bytes from 150.109.19.98: seq=4 ttl=37 time=65.332 ms

--- tuture.co ping statistics ---
5 packets transmitted, 3 packets received, 40% packet loss
round-trip min/avg/max = 65.294/65.350/65.425 ms

OK,虽然丢了几个包,可是能够连上(取决于你的网络环境,全丢包也是正常的)。因而可知,容器内能够访问主机所链接的所有网络(包括 localhost)。

接下来测试可否链接到 alpine2,在刚才 docker network inspect 命令的输出中找到 alpine2 的 IP 为 172.17.0.3,尝试可否 ping 通:

/ # ping -c 5 172.17.0.3
PING 172.17.0.3 (172.17.0.3): 56 data bytes
64 bytes from 172.17.0.3: seq=0 ttl=64 time=0.147 ms
64 bytes from 172.17.0.3: seq=1 ttl=64 time=0.103 ms
64 bytes from 172.17.0.3: seq=2 ttl=64 time=0.102 ms
64 bytes from 172.17.0.3: seq=3 ttl=64 time=0.125 ms
64 bytes from 172.17.0.3: seq=4 ttl=64 time=0.125 ms

--- 172.17.0.3 ping statistics ---
5 packets transmitted, 5 packets received, 0% packet loss
round-trip min/avg/max = 0.102/0.120/0.147 ms

完美!咱们可以从 alpine1 中访问 alpine2 容器。做为练习,你能够本身尝试一下可否从 alpine2 容器中 ping 通 alpine1 哦。

注意

若是你不想让 alpine1 停下来,记得经过 Ctrl + P + Ctrl + Q(按住 Ctrl,而后依次按 P 和 Q 键)“脱离”(detach,也就是刚才 attach 命令的反义词)容器,而不是按 Ctrl + D 哦。

自定义网络

若是你跟着上面一路试下来,会发现默认的 bridge 网络存在一个很大的问题:只能经过 IP 地址相互访问。这毫无疑问是很是麻烦的,当容器数量不少的时候难以管理,并且每次的 IP 均可能发生变化。

而自定义网络则很好地解决了这一问题。在同一个自定义网络中,每一个容器可以经过彼此的名称相互通讯,由于 Docker 为咱们搞定了 DNS 解析工做,这种机制被称为服务发现(Service Discovery)。具体而言,咱们将建立一个自定义网络 my-net,并建立 alpine3alpine4 两个容器,连上 my-net,以下图所示。

让咱们开始动手吧。首先建立自定义网络 my-net

docker network create my-net
# 因为默认网络驱动为 bridge,所以至关于如下命令
# docker network create --driver bridge my-net

查看当前全部的网络:

docker network ls

能够看到刚刚建立的 my-net

NETWORK ID          NAME                DRIVER              SCOPE
cb33efa4d163        bridge              bridge              local
010deedec029        host                host                local
feb13b480be6        my-net              bridge              local
772a7a450223        none                null                local

建立两个新的容器 alpine3alpine4

docker run -dit --name alpine3 --network my-net alpine
docker run -dit --name alpine4 --network my-net alpine

能够看到,咱们经过 --network 参数指定容器想要链接的网络(也就是刚才建立的 my-net)。

提示

若是在一开始建立并运行容器时忘记指定网络,那么下次再想指定网络时,能够经过 docker network connect 命令再次连上(第一个参数是网络名称 my-net,第二个是须要链接的容器 alpine3):

docker network connect my-net alpine3

进入到 alpine3 中,测试可否 ping 通 alpine4

$ docker attach alpine3
/ # ping -c 5 alpine4
PING alpine4 (172.19.0.3): 56 data bytes
64 bytes from 172.19.0.3: seq=0 ttl=64 time=0.247 ms
64 bytes from 172.19.0.3: seq=1 ttl=64 time=0.176 ms
64 bytes from 172.19.0.3: seq=2 ttl=64 time=0.180 ms
64 bytes from 172.19.0.3: seq=3 ttl=64 time=0.176 ms
64 bytes from 172.19.0.3: seq=4 ttl=64 time=0.161 ms

--- alpine4 ping statistics ---
5 packets transmitted, 5 packets received, 0% packet loss
round-trip min/avg/max = 0.161/0.188/0.247 ms

能够看到 alpine4 被自动解析成了 172.19.0.3。咱们能够经过 docker network inspect 来验证一下:

$ docker network inspect --format '{{range .Containers}}{{.Name}}: {{.IPv4Address}} {{end}}' my-net
alpine4: 172.19.0.3/16 alpine3: 172.19.0.2/16

能够看到 alpine4 的 IP 的确为 172.19.0.3,解析是正确的!

一些收尾工做

实验作完了,让咱们把以前全部的容器所有销毁:

docker rm -f alpine1 alpine2 alpine3 alpine4

把建立的 my-net 也删除:

docker network rm my-net

动手实践

容器化服务器

咱们首先对后端服务器也进行容器化。建立 server/Dockerfile,代码以下:

FROM node:10

# 指定工做目录为 /usr/src/app,接下来的命令所有在这个目录下操做
WORKDIR /usr/src/app

# 将 package.json 拷贝到工做目录
COPY package.json .

# 安装 npm 依赖
RUN npm config set registry https://registry.npm.taobao.org && npm install

# 拷贝源代码
COPY . .

# 设置环境变量(服务器的主机 IP 和端口)
ENV MONGO_URI=mongodb://dream-db:27017/todos
ENV HOST=0.0.0.0
ENV PORT=4000

# 开放 4000 端口
EXPOSE 4000

# 设置镜像运行命令
CMD [ "node", "index.js" ]

能够看到这个 Dockerfile 比上一篇教程中的要复杂很多。每一行的含义已经注释在代码中了,咱们来看一看多了哪些新东西:

  • RUN 指令用于在容器中运行任何命令,这里咱们经过 npm install 安装全部项目依赖(固然以前配置了一下 npm 镜像,能够安装得快一点)
  • ENV 指令用于向容器中注入环境变量,这里咱们设置了 数据库的链接字符串 MONGO_URI注意这里给数据库取名为 dream-db,后面就会建立这个容器),还配置了服务器的 HOSTPORT
  • EXPOSE 指令用于开放端口 4000。以前在用 Nginx 容器化前端项目时没有指定,是由于 Nginx 基础镜像已经开放了 8080 端口,无需咱们设置;而这里用的 Node 基础镜像则没有开放,须要咱们本身去配置
  • CMD 指令用于指定此容器的启动命令(也就是 docker ps 查看时的 COMMAND 一列),对于服务器来讲固然就是保持运行状态。在后面“回忆与升华”部分会详细展开。
注意

初次尝试容器的朋友很容易犯的一个错误就是忘记将服务器的 hostlocalhost127.0.0.1)改为 0.0.0.0,致使服务器没法在容器以外被访问到(我本身学习的时候也浪费了不少时间)。

与以前前端容器化相似,建立 server/.dockerignore 文件,忽略服务器日志 access.lognode_modules,代码以下:

node_modules
access.log

在项目根目录下运行如下命令,构建服务器镜像,指定名称为 dream-server

docker build -t dream-server server

链接服务器与数据库

根据以前的知识,咱们为如今的“梦想清单”应用建立一个自定义网络 dream-net

docker network create dream-net

咱们使用官方的 mongo 镜像建立并运行 MongoDB 容器,命令以下:

docker run --name dream-db --network dream-net -d mongo

咱们指定容器名称为 dream-db(还记得这个名字吗),所链接的网络为 dream-net,而且在后台模式下运行(-d)。

提示

你也许会问,为何这里开启容器的时候没有指定端口映射呢?由于在同一自定义网络中的全部容器会互相暴露全部端口,不一样的应用之间能够更轻松地相互通讯;同时,除非经过 -p--publish)手动开放端口,网络以外没法访问网络中容器的其余端口,实现了良好的隔离性。网络以内的互操做性网络内外的隔离性也是 Docker Network 的一大优点所在。

危险!

这里咱们在开启 MongoDB 数据库容器时没有设置任何鉴权措施(例如设置用户名和密码),全部链接数据库的请求均可以任意修改数据,在生产环境是极其危险的。后续文章中咱们会讲解如何在容器中管理机密信息(例如密码)。

而后运行服务器容器:

docker run -p 4000:4000 --name dream-api --network dream-net -d dream-server

查看服务器容器的日志输出,肯定 MongoDB 链接成功:

$ docker logs dream-api                                                       
Server is running on http://0.0.0.0:4000
Mongoose connected.

接着你能够经过 Postman 或者 curl 来测试一波服务器 API (localhost:4000 ),这里为了节约篇幅就省略了。固然你也能够直接跳过,由于立刻咱们就能够经过前端来操做数据了!

容器化前端页面

正如上一篇文章所实现的那样,在项目根目录下,经过如下命令进行容器化:

docker build -t dream-client client

而后运行容器:

docker run -p 8080:80 --name client -d dream-client

能够经过 docker ps 命令检验三个容器是否所有正确开启:

最后,访问 localhost:8080

能够看到,咱们在最后刷新了几回页面,数据记录也都还在,说明咱们带有数据库的全栈应用跑起来了!让咱们经过交互式执行的方式进入到数据库容器 dream-db 中,经过 Mongo Shell 简单地查询一波刚才的数据:

$ docker exec -it dream-db mongo
MongoDB shell version v3.4.10
connecting to: mongodb://127.0.0.1:27017
MongoDB server version: 3.4.10
Welcome to the MongoDB shell.
For interactive help, type "help".
> use todos
switched to db todos
> db.getCollection('todos').find()
{ "_id" : ObjectId("5e171fda820251a751aae6f5"), "completed" : true, "text" : "了解 Docker Network", "timestamp" : ISODate("2020-01-09T12:43:06.865Z"), "__v" : 0 }
{ "_id" : ObjectId("5e171fe08202517c11aae6f6"), "completed" : true, "text" : "搭建默认网络", "timestamp" : ISODate("2020-01-09T12:43:12.205Z"), "__v" : 0 }
{ "_id" : ObjectId("5e171fe3820251d1a4aae6f7"), "completed" : false, "text" : "搭建自定义网络", "timestamp" : ISODate("2020-01-09T12:43:15.962Z"), "__v" : 0 }

完美!而后按 Ctrl + D 就能够退出来了。

回忆与升华

理解命令:梦境的主旋律

每一个容器自从被建立之时,就注定要运行一道命令(Command),就好像在筑梦时要安排一个主旋律、一个基调那样。以前在运行 docker ps 的时候,你应该也注意到了 COMMAND 一栏,正是每一个容器所运行的命令。那么咱们怎么指定容器的命令呢?又能不能运行新的命令呢?

首先,咱们主要经过两种方式指定容器的命令:

经过 Dockerfile 提供默认命令

在构建镜像时,咱们能够在 Dockerfile 的最后经过 CMD 指令指定命令,例如在构建后端服务器时的 [ "node", "server.js" ] 命令。在指定命令时,咱们有三种写法:

  • CMD ["executable","param1","param2"](exec 格式,推荐
  • CMD ["param1","param2"](须要结合 Entrypoint 使用)
  • CMD command param1 param2(shell 格式)

其中 executable 表明可执行文件的路径,例如 node/bin/shparam1param2 表明参数。咱们在后续讨论 Dockerfile 的高阶使用时会讨论 Entrypoint 的使用,这篇文章不会涉及

注意

在使用第一种 exec 格式时,必须使用双引号,由于整个命令将以 JSON 格式被解析。

提示

若是要执行变量替换等 Shell 操做,例如 echo $HOME,直接写成 ["echo", "$HOME"] 是无效的,须要改写成 ["sh", "-c", "echo $HOME"]

建立或运行容器时指定命令

在建立或运行容器时,经过添加命令参数能够覆盖构建镜像时指定的命令,例如:

docker run nginx echo hello

经过指定 echo hello 命令参数,就会让这个容器输出一个 hello 而后退出,而不会运行默认的 nginx -g 'daemon off;'

固然,正如第一篇文章所实践的,咱们还能够指定命令为 bash(或 shmongonode 等其余交互式程序),而后结合 -it 选项,就能够进入容器中交互式运行了。

经过 exec 运行新的命令

经过 docker exec,咱们可让已经运行中的容器执行新的命令。例如,对于咱们以前的 dream-db 容器,咱们经过 mongodump 命令来建立数据库备份:

docker exec dream-db mongodump

而后能够进一步经过 docker exec -it 来进入 dream-db 中进行交互式运行,检查刚才导出的 dump 目录:

$ docker exec -it dream-db bash
root@c51d9355d8da:/# ls dump/
admin  todos

一样地,按 Ctrl + D 退出就能够了。

提示

你也许会好奇,为何在 docker run 交互式执行的时候按 Ctrl + D 就容器就直接中止了,而在 docker exec 的状况下退出却不会致使容器中止呢?由于 docker exec -it 至关于在现有的容器上运行了一个新的终端进程,而不会影响以前的主命令进程。只要主进程不结束,容器就不会中止。

小诀窍:如何轻松记住几十个 Docker 命令?

在刚才的实战中,咱们也接触了不少新的 Docker 命令,怎么记住那么多命令呢?其实 docker 大部分命令都符合如下格式:

docker <对象类型> <操做名称> [其余选项和参数]
  • 对象类型:到目前,咱们接触的 Docker 对象类型包括容器
    container镜像 image网络 network
  • 操做名称:操做能够分为两大类:1)适用于全部对象的操做,例如 lsrminspectprune 等等;2)对象专属操做,例如容器专有的 run 操做,镜像专有的 build 操做,以及网络专有的 connect 操做等等
  • 其余选项和参数:可经过 help 命令或 --help 查阅每一个命令具体的选项和参数

因为部分命令很经常使用,Docker 还提供了方便的简写命令,例如显示当前全部容器 docker container ls,能够简写成 docker ps

咱们首先复习一下容器(Container)对象上的命令吧(红色表明适用于全部对象的操做,蓝色表明此对象的专有操做):

再复习一下镜像(Image)对象上的命令:

最后复习一下网络(Network)对象上的命令:

至此,这篇教程也结束了。可是咱们的筑梦之旅才刚刚开始——还有不少问题没有解决:1)如今前端应用还没法在除了本地之外的环境使用(由于访问的后端 API 是硬编码的 localhost);2)尚未真正部署到远程机器;3)MongoDB 还处于“裸奔”的状态(没设置密码)。不要方,咱们在接下里的教程中就会去解决哦。

想要学习更多精彩的实战技术教程?来 图雀社区逛逛吧。

相关文章
相关标签/搜索