目录

隐藏
  1. 怎么固定容器 IP 地址?每次重启容器都要变化 IP 地址怎么办?
  2. 我要映射好几百个端口,难道要一个个 -p 么?
  3. 容器怎么取宿主机 IP 啊?
  4. 如何修改容器的 /etc/hosts 文件?
  5. 怎么映射宿主端口?Dockerfile 中的EXPOSE和 docker run -p 有啥区别?
  6. 听说 --link 过时不再用了?那容器互联、服务发现怎么办?
  7. Docker 多宿主网络怎么配置?
  8. 如何让一个容器连接两个网络?
  9. 为什么 -p 后还是无法通过映射端口访问容器里面的服务?
  10. 明明 docker network ls 中看到了建立的 overlay 网络,怎么 docker run 还说网络不存在啊?
怎么固定容器 IP 地址?每次重启容器都要变化 IP 地址怎么办?
一般情况是不需要指定容器 IP 地址的。这不是虚拟主机,而是容器。其地址是供容器间通讯的,容器间则不用 IP 直接通讯,而使用容器名、服务名、网络别名。

为了保持向后兼容,docker run 在不指定 --network 时,所在的网络是 default bridge,在这个网络下,需要使用 --link 参数才可以让两个容器找到对方。

这是有局限性的,因为这个时候使用的是 /etc/hosts 静态文件来进行的解析,比如一个主机挂了后,重新启动IP可能会改变。虽然说这种改变Docker是可能更新/etc/hosts文件,但是这有诸多问题,可能会因为竞争冒险导致 /etc/hosts 文件损毁,也可能还在运行的容器在取得 /etc/hosts 的解析结果后,不再去监视该文件是否变动。种种原因都可能会导致旧的主机无法通过容器名访问到新的主机。

参考官网文档: https://docs.docker.com/engine/userguide/networking/default_network/dockerlinks/

如果可能不要使用这种过时的方式,而是用下面说的自定义网络的方式。

而对于新的环境(Docker 1.10以上),应该给容器建立自定义网络,同一个自定义网络中,可以使用对方容器的容器名、服务名、网络别名来找到对方。这个时候帮助进行服务发现的是Docker 内置的DNS。所以,无论容器是否重启、更换IP,内置的DNS都能正确指定到对方的位置。

参考官网文档: https://docs.docker.com/engine/userguide/networking/work-with-networks/#linking-containers-in-user-defined-networks

建议参考一下我写的 LNMP 的例子:
https://coding.net/u/twang2218/p/docker-lnmp/git

我要映射好几百个端口,难道要一个个 -p 么?
-p 是可以用范围的:

-p 8001-8010:8001-8010


容器怎么取宿主机 IP 啊?
单机环境

如果是单机环境,很简单,不必琢磨怎么突破命名空间限制,直接用环境变量送进去即可。

docker run -d -e HOST_IP=<宿主的IP地址> nginx
然后容器内直接读取 HOST_IP 环境变量即可。

集群环境

集群环境相对比较复杂,docker service create 中的 -e 以及 --env-file是在服务创建时指定、读取环境变量内容,而不是运行时,因此对于每个节点都是一样的。而且目前不存在 dockerd -e 选项,所以直接使用这些选项达不到我们想要的效果。

不过有变通的办法,可以在宿主上建立一个 /etc/variables 文件(名字随意,这里用这个文件举例)。其内容为:

HOST_IP=1.2.3.4
其中 1.2.3.4 是这个节点的宿主 IP,因此每个节点的 /etc/variables 的内容不同。

而在启动服务时,指定挂载这个服务端本地文件:

docker service create --name app \
--mount type=bind,source=/etc/variables,target=/etc/variables:ro \
myapp
由于 --mount 是发生于容器运行时,因此所加载的是所运行的服务器的 /etc/variables,里面所包含的也是该服务器的 IP 地址。

在 myapp 这个镜像的入口脚本加入加载该环境变量文件的命令:

source /etc/variables
这样 app 这个服务容器就会拥有 HOST_IP 环境变量,其值为所运行的宿主 IP。

如何修改容器的 /etc/hosts 文件?
容器内的 /etc/hosts 文件不应该被随意修改,如果必须添加主机名和 IP 地址映射关系,应该在 docker run 时使用 --add-host 参数,或者在 docker-compose.yml 中添加 extra_hosts 项。

不过在用之前,应该再考虑一下真的需要修改 /etc/hosts 么?如果只是为了容器间互相访问,应该建立自定义网络,并使用 Docker 内置的 DNS 服务。

可以参考一下我写的这个 LNMP 多容器互连的例子: https://coding.net/u/twang2218/p/docker-lnmp/git

怎么映射宿主端口?Dockerfile 中的EXPOSE和 docker run -p 有啥区别?
Docker中有两个概念,一个叫做 EXPOSE ,一个叫做 PUBLISH 。

EXPOSE 是镜像/容器声明要暴露该端口,可以供其他容器使用。这种声明,在没有设定 --icc=false的时候,实际上只是一种标注,并不强制。也就是说,没有声明 EXPOSE 的端口,其它容器也可以访问。但是当强制 --icc=false 的时候,那么只有 EXPOSE 的端口,其它容器才可以访问。
PUBLISH 则是通过映射宿主端口,将容器的端口公开于外界,也就是说宿主之外的机器,可以通过访问宿主IP及对应的该映射端口,访问到容器对应端口,从而使用容器服务。
EXPOSE 的端口可以不 PUBLISH,这样只有容器间可以访问,宿主之外无法访问。而 PUBLISH 的端口,可以不事先 EXPOSE,换句话说 PUBLISH 等于同时隐式定义了该端口要 EXPOSE。

docker run 命令中的 -p, -P 参数,以及 docker-compose.yml 中的  ports 部分,实际上均是指 PUBLISH。

小写 -p 是端口映射,格式为 [宿主IP:]<宿主端口>:<容器端口>,其中宿主端口和容器端口,既可以是一个数字,也可以是一个范围,比如:1000-2000:1000-2000。对于多宿主的机器,可以指定宿主IP,不指定宿主IP时,守护所有接口。

大写 -P 则是自动映射,将所有定义 EXPOSE 的端口,随机映射到宿主的某个端口。

听说 --link 过时不再用了?那容器互联、服务发现怎么办?
在 1-2 年前,Docker 所有容器都连接于默认的桥接网络上,也就是很多老文章鼓捣的 docker0 桥接网卡。因此实际上默认情况下所有容器都是可以互联的,没有隔离,当然这样安全性不好。而服务发现,是在这种环境下发展出来的,通过修改容器内的 /etc/hosts 文件来完成的。凡是 --link 的主机的别名就会出现于 /etc/hosts 中,其地址由 Docker 引擎维护。因此容器间才可以通过别名互访。

但是这种办法并不是好的解决方案,Docker 早在一年多以前就已经使用自定义网络了。在同一个网络中的容器,可以互联,并且,Docker 内置了 DNS,容器内的应用可以使用服务名、容器名、别名来进行服务发现,名称会经由内置的 DNS 进行解析,其结果是动态的;而不在同一网络中的容器,不可以互联。

因此,现在早就不用 --link 了,而且非常不建议使用。

首先是因为使用 --link 就很可能还在用默认桥接网络,这很不安全,所有容器都没有适度隔离,用自定义网络才比较方便互联隔离。

其次,修改 /etc/hosts 文件有很多弊病。比如,高频繁的容器启停环境时,容易产生竞争冒险,导致 /etc/hosts 文件损坏,出现访问故障;或者有些应用发现是来自于 /etc/hosts 文件后,就假定其为静态文件,而缓存结果不再查询,从而导致容器启停 IP 变更后,使用旧的条目而无法连接到正确的容器等等。

另外,在一代 Swarm 环境中,在 docker-compose.yml 中使用了 links 就意味着服务间的强依赖关系,因此调度时不会将服务运行于不同节点,而是全部运行于一个节点,使得横向扩展失败。

所以不要再使用 --link 以及 docker-compose.yml 中的 links 了。应该使用 docker network,建立网络,而 docker run --network 来连接特定网络。或者使用 version: '2' 的 docker-compose.yml 直接定义自定义网络并使用。

建议去看一下我写的 LNMP 多容器互联的例子: https://coding.net/u/twang2218/p/docker-lnmp/git

Docker 多宿主网络怎么配置?
Docker 跨节点容器网络互联,最通用的是使用 overlay 网络。

一代 Swarm 已经不再使用,它要求使用 overlay 网络前先准备好分布式键值库,比如 etcd, consul 或 zookeeper。然后在每个节点的 Docker 引擎中,配置 --cluster-store 和 --cluster-advertise 参数。这样才可以互连。可以参考我写的 LNMP 容器互联例子中的 run1.sh 这个脚本,这个脚本是利用 docker-machine自动建立 Swarm 并且配置好 overlay 的脚本,可以分析其流程。

现在都在使用二代 Swarm,也就是 Docker Swarm Mode,非常简单,只要 docker swarm init 建立集群,其它节点 docker swarm join 加入集群后,集群内的服务就自动建立了 overlay 网络互联能力。

需要注意的是,如果是多网卡环境,无论是 docker swarm ini 还是 docker swarm join,都不要忘记使用参数 --advertise-addr 指定宣告地址,否则自动选择的地址很可能不是你期望的,从而导致集群互联失败。格式为 --advertise-addr <地址>:<端口>,地址可以是 IP 地址,也可以是网卡接口,比如 eth0。端口默认为 2377,如果不改动可以忽略。

此外,这是供服务使用的 overlay,因此所有 docker service create 的服务容器可以使用该网络,而 docker run 不可以使用该网络,除非明确该网络为 --attachable。

关于 overlay 网络的进一步信息,可以参考官网文档: https://docs.docker.com/engine/userguide/networking/get-started-overlay/

虽然默认使用的是 overlay 网络,但这并不是唯一的多宿主互联方案。Docker 内置了一些其它的互联方案,比如效率比较高的 macvlan。如果在局域网络环境下,对 overlay 的额外开销不满意,那么可以考虑 macvlan 以及 ipvlan,这是比较好的方案。
https://docs.docker.com/engine/userguide/networking/get-started-macvlan/

此外,还有很多第三方的网络可以用来进行跨宿主互联,可以访问官网对应文档进一步查看: https://docs.docker.com/engine/extend/legacy_plugins/#/network-plugins

如何让一个容器连接两个网络?
如果是使用 docker run,那很不幸,一次只可以连接一个网络,因为 docker run 的 --network 参数只可以出现一次(如果出现多次,最后的会覆盖之前的)。不过容器运行后,可以用命令 docker network connect 连接多个网络。

假设我们创建了两个网络:

$ docker network create mynet1
$ docker network create mynet2
然后,我们运行容器,并连接这两个网络。

$ docker run -d --name web --network mynet1 nginx
$ docker network connect mynet2 web
但是如果使用 docker-compose 那就没这个问题了。因为实际上, Docker Remote API 是支持一次性指定多个网络的,但是估计是命令行上不方便,所以 docker run 限定为只可以一次连一个。docker-compose 直接就可以将服务的容器连入多个网络,没有问题。

version: '2'
services:
web:
image: nginx
networks:
- mynet1
- mynet2
networks:
mynet1:
mynet2:


为什么 -p 后还是无法通过映射端口访问容器里面的服务?
首先,当然是检查这个 docker 的容器是否启动正常: docker ps、docker top <容器ID>、docker logs <容器ID>、docker exec -it <容器ID> bash等,这是比较常用的排障的命令;如果是 docker-compose 也有其对应的这一组命令,所以排障很容易。

如果确保服务一切正常,甚至在容器里,可以访问到这些服务,docker ps 也显示出了端口映射成功,那么就需要检查防火墙了。

本机防火墙

在 Docker 运行的系统上不应该运行任何防火墙……没错,说你呢,CentOS 的 firewalld 和 Ubuntu 的 ufw 同学。由于 Docker 使用 iptables 规则来进行网络数据流的控制,而那些防火墙总以为只有自己撰写 iptables,从而经常会导致 Docker 设置了一些规则,然后转眼就被 firewalld 或 ufw 给清了,特别是起、停防火墙服务的时候。从而导致 Docker 的网络从外界无法访问。

为了避免 iptables 的规则干扰,不要在运行 Docker 的服务器上,运行任何防火墙或配置自定义的 iptables 规则,除非你非常清楚你在做什么,并且知道会产生什么后果。另外,关闭防火墙后,记得重启系统,至少是重启 Docker 服务。否则防火墙的起、停、刷新这类行为会导致清空 Docker 设置的网络规则,而导致容器内的网络无法和外部互联。

边界防火墙

如果你使用的是云服务器,那么除了本机防火墙外,云服务的服务商一般会提供边界防火墙服务,比如安全组、安全策略类的东西。有些服务器为了安全起见,默认只开通必需的 22 端口给 SSH 使用,而其它端口屏蔽。这也是可能导致远程访问服务器 -p 端口失败的原因之一。如果你发现你在服务器本地访问服务,比如 curl localhost 没有阻碍,但是远程访问该服务就连接失败的话,那么应该去检查云服务商的安全设置,是否忘记了开启所需的端口。

明明 docker network ls 中看到了建立的 overlay 网络,怎么 docker run 还说网络不存在啊?
如果在 docker network ls 中看到了如下的 overlay 网络:

NETWORK ID          NAME                DRIVER              SCOPE
...
24pz359114y0        mynet               overlay             swarm
...
那么这个名为 mynet 的网络是不可以连接到 docker run 的容器。如果试图连接则会出现报错。

如果是 1.12 的系统,会看到这样报错信息:

$ docker run --rm --network mynet busybox
docker: Error response from daemon: network mynet not found.
See 'docker run --help'.
报错说 mynet 网络找不到。其实如果仔细观察,会看到这个名为 mynet 的网络,驱动是 overlay 没有错,但它的 Scope 是 swarm。这个意思是说这个网络是在二代 Swarm 环境中建立的 overlay 网络,因此只可以由 Swarm 环境下的服务容器才可以使用。而 docker run 所运行的只是零散的容器,并非 Service,因此自然在零散容器所能使用的网络中,不存在叫 mynet 网络。

docker run 可以使用的 overlay 网络是 Scope 为 global 的 overlay 网络,也就是使用外置键值库所建立的 overlay 网络,比如一代 Swarm 的 overlay 网络。

这点在 1.13 后稍有变化。如果是 1.13 以后的系统,会看到这样的信息:

$ docker run --rm --network mynet busybox
docker: Error response from daemon: Could not attach to network mynet: rpc error: code = 7
desc = network mynet not manually attachable.
报错信息不再说网络找不到,而是说这个 mynet 网络无法连接。这是由于从 1.13 开始,允许在建立网络的时候声明这个网络是否可以被零散的容器所连接。如果 docker network create 加了 --attachable 的参数,那么在后期,这个网络是可以被普通容器所连接的。

但是这是在安全模型上开了一个口子,因此,默认不允许普通容器链接,并且不建议使用。