Cheney的个人博客

关于Docker的网络配置

2020-02-07 · 7 min read
docker-compose Docker

最近实习的一个小项目中涉及到了使用docker部署。正好之前一直想学习下docker,于是本着业务驱动学习的心态,花了1天速成了一下docker。然而,当我开始着手配置容器的时候,容器间的通信和网络死活配置不好。最终,经历了整整2天的查阅文档和尝试,才完成了老大的要求(好菜)。关于这个问题的描述网上并不多,于是我决定写一篇博客来记录一下。

问题背景

为了快速部署和迭代,整个项目采用了docker化部署的策略。简单来说就是一个应用模块打包成一个docker容器:react的前端一个,django一个,java的后端一个,MySQL一个,nginx一个...

一般来说流程都是用docker-compose来编排和部署一组容器,这一组容器相当于封装在了一起,共享各种资源,就好像在同一台虚拟机上运行一样。但是偶尔,我们会遇到这样的需求:为了降低代码的耦合度,要求将不同的应用组封装在不同的docker-compose,但是仍然需要互相通信。简言之,两个docker-compose文件中的容器如何互相通信呢?

问题描述

让我们简化一下模型(略去了部分细节和多余的容器):

# db/docker-compose-1.yml
# MySQL
version: "3"
services:
  db:
    container_name: my-db
    image: mysql
    ports:
      - "3306:3306"
# app/docker-compose-2.yml
# django app
version: "3"
services:
  web:
    container_name: my-app
    build: .
    command: python manage.py runserver 0.0.0.0:8000
    ports:
      - "8000:8000"

简单来说,我现在有两个docker-compose文件,分别run起来之后,我希望实现我的django app可以访问MySQL数据库(在3306端口)。

乍一看,好像分别run起来就行了呀,反正MySQL已经映射到本机的3306端口,django只要去连接localhost的3306端口就好了嘛!

其实不然。docker-compose会为每一个文件创建一个network(docker的虚拟网络),把每一个文件里的所有容器连接在一个网络。也就是说,上面的两个容器实际上处于不同的网络!而众所周知,容器化技术可以近似的认为是虚拟机技术,所以每个容器和宿主机也没什么关系。一句话总结:MySQL所在容器的localhost、django所在容器的localhost,和你宿主机的localhost,这三个都不一样!!!

问题解决

众所周知(?),docker的网络类型一般有4种(还有个神奇的就不提了):

  • Bridge:默认模式,虚拟网桥,一个网桥内的容器可以互相通信。
  • Host:直接与宿主机共用网络,也就是localhost直接变成宿主机。(但是该模式只有linux支持,且该模式与ports和expose选项不兼容,因为反正已经融为一体了,所有端口与主机相同)
  • Overlay:跨主机的容器间相互通信。
  • None:没有任何网络。

那么最粗暴的解决方式当然是统统Host。但是这样很不优雅,容器化的意义就被弱化了。而且要mac和windows用户怎么办?(作为一个mac用户其实一开始尝试过host,半天之后才在文档发现一行小字说只支持linux...)

查阅了文档之后发现了这样一种神奇操作,只要在docker-compose-2.yml后面加上:

# app/docker-compose-2.yml
# django app
version: "3"
services:
  web:
    container_name: my-app
    build: .
    command: python manage.py runserver 0.0.0.0:8000
    ports:
      - "8000:8000"
networks:
  default:
    external:
      name: db_default

这是一个全局的networks设置(和services同级),意思是把默认的全局网络设置为一个叫db_default的网络(external的意思是去找现存的网络,而不是新建一个)。

那为什么网络的名字叫db_default呢?原因是每一个docker-compose启动的时候会创建一个叫[projectname]_default的网络,projectname就是这个文件所在的文件夹名字。例如上面两个docker-compose分别启动的话,会产生一个db_default和一个app_default。现在的操作就是阻止了app_default的生成,而让它加入现存的db_default(当然这也意味着db要在web前启动)。

具体的网络名字可以到命令行输入docker network ls看到:

当然如果你觉得db_default这个自动生成的名字不太保险,你也可以自己设置:

# db/docker-compose-1.yml
# MySQL
version: "3"
services:
  db:
    container_name: my-db
    image: mysql
    networks:
      - my_net
    ports:
      - "3306:3306"
networks:
  my_net:
    name: my_net

上面的networks在db的下级,表示加入一个叫my_net的网络,而下面那个全局的networks表示创建一个叫my_net的网络。此时db_default不再存在。

# app/docker-compose-2.yml
# django app
version: "3"
services:
  web:
    container_name: my-app
    build: .
    command: python manage.py runserver 0.0.0.0:8000
    ports:
      - "8000:8000"
networks:
  default:
    external:
      name: my_net

然后再修改下django的docker-compose设置,让它也去找现存的my_net。就此,容器间的网络配置就解决了。这两个容器现在连接到了一个网络中,我是不是就能在django中通过localhost连接数据库了呢?

还是不能。此时如果运行docker network inspect my_net查看网络设置,会在Containers一项下发现,这两个容器虽然连到了一个网络里,但是ip地址仍然不同,就类似于一个局域网里的两台机器。

但不要担心,当两个container连接到一个网络后,容器名会变成自己的hostname,以区分不同的容器。此时就可以通过容器名来访问数据库,只需要修改下django的配置文件:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'mysql',
        'USER': 'root',
        'PASSWORD': '****',
        # Hostname must be container name.
        'HOST': 'my-db',
        'PORT': '3306',
    }
}

也就是完整的mysql地址从mysql://127.0.0.1:3306/变成了mysql://my-db:3306/。这里my-db相当于域名,最终还是会解析成这个容器在局域网里的ip。

总结与补充

  1. 最终的效果是实现了两个docker-compose文件内的容器通信,而没有与宿主机的网络产生过多的交集,端口依然整洁,保证了容器与容器间,容器与宿主机间的隔离。
  2. 不推荐使用network_mode来设置,虽然也有container或者service这个选项,但是会与ports设置冲突!你就没办法把django的端口暴露出来了!(除非你把django的端口暴露设置到db那个service去,但这样一来就耦合了)
  3. 不推荐links或者external_links,这两个都是legacy option,未来会被移除。
  4. 要注意下django的端口是跑在0.0.0.0:8000,如果命令只写8000或者不指明端口号,还是会跑在127.0.0.1,端口暴露是映射不出去的!
  5. 事实证明看文档速成是会出问题的。

Reference

[1] https://docs.docker.com/compose/networking/
[2] https://docs.docker.com/compose/compose-file/