在实际应用中,不一样的服务之间是须要通讯的,例如后端 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,顾名思义就是“网络”,可以让不一样的容器之间相互通讯。首先有必要要列举一下 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
网桥网络可分为两类:数据库
让咱们分别实践一下吧。
这个小实验的内容以下图所示:
咱们会在默认的 bridge
网络上链接两个容器 alpine1
和 alpine2
。 运行如下命令,查看当前已有的网络:
docker network ls
应该会看到如下输出(注意你机器上的 ID 颇有可能不同):
NETWORK ID NAME DRIVER SCOPE cb33efa4d163 bridge bridge local 010deedec029 host host local 772a7a450223 none null local
这三个默认网络分别对应上面的 bridge
、host
和 none
网络类型。接下来咱们将建立两个容器,分别名为 alpine1
和 alpine2
,命令以下:
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
:包括此网络上链接的全部容器,能够看到咱们刚刚建立的 alpine1
和 alpine2
,它们的 IP 地址分别为 172.17.0.2
和 172.17.0.3
(后面的 /16
是子网掩码,暂时不用考虑)提示
若是你熟悉 Go 模板语法,能够经过
-f
(format
)参数过滤掉不须要的信息。例如咱们只想查看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
,并建立 alpine3
和 alpine4
两个容器,连上 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
建立两个新的容器 alpine3
和 alpine4
:
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
,后面就会建立这个容器),还配置了服务器的 HOST
和 PORT
EXPOSE
指令用于开放端口 4000。以前在用 Nginx 容器化前端项目时没有指定,是由于 Nginx 基础镜像已经开放了 8080 端口,无需咱们设置;而这里用的 Node 基础镜像则没有开放,须要咱们本身去配置CMD
指令用于指定此容器的启动命令(也就是 docker ps
查看时的 COMMAND 一列),对于服务器来讲固然就是保持运行状态。在后面“回忆与升华”部分会详细展开。注意初次尝试容器的朋友很容易犯的一个错误就是忘记将服务器的
host
从localhost
(127.0.0.1
)改为0.0.0.0
,致使服务器没法在容器以外被访问到(我本身学习的时候也浪费了不少时间)。
与以前前端容器化相似,建立 server/.dockerignore
文件,忽略服务器日志 access.log
和 node_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
的最后经过 CMD
指令指定命令,例如在构建后端服务器时的 [ "node", "server.js" ]
命令。在指定命令时,咱们有三种写法:
CMD ["executable","param1","param2"]
(exec 格式,推荐)CMD ["param1","param2"]
(须要结合 Entrypoint 使用)CMD command param1 param2
(shell 格式)其中 executable
表明可执行文件的路径,例如 node
、/bin/sh
;param1
、param2
表明参数。咱们在后续讨论 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
(或 sh
、mongo
、node
等其余交互式程序),而后结合 -it
选项,就能够进入容器中交互式运行了。
经过 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 <对象类型> <操做名称> [其余选项和参数]
container
、镜像 image
和网络 network
ls
、rm
、inspect
和 prune
等等;2)对象专属操做,例如容器专有的 run
操做,镜像专有的 build
操做,以及网络专有的 connect
操做等等help
命令或 --help
查阅每一个命令具体的选项和参数因为部分命令很经常使用,Docker 还提供了方便的简写命令,例如显示当前全部容器 docker container ls
,能够简写成 docker ps
。
咱们首先复习一下容器(Container)对象上的命令吧(红色表明适用于全部对象的操做,蓝色表明此对象的专有操做):
再复习一下镜像(Image)对象上的命令:
最后复习一下网络(Network)对象上的命令:
至此,这篇教程也结束了。可是咱们的筑梦之旅才刚刚开始——还有不少问题没有解决:1)如今前端应用还没法在除了本地之外的环境使用(由于访问的后端 API 是硬编码的 localhost
);2)尚未真正部署到远程机器;3)MongoDB 还处于“裸奔”的状态(没设置密码)。不要方,咱们在接下里的教程中就会去解决哦。
想要学习更多精彩的实战技术教程?来 图雀社区逛逛吧。