最近实习的一个小项目中涉及到了使用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种(还有个神奇的就不提了):
那么最粗暴的解决方式当然是统统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。
0.0.0.0:8000
,如果命令只写8000或者不指明端口号,还是会跑在127.0.0.1
,端口暴露是映射不出去的![1] https://docs.docker.com/compose/networking/
[2] https://docs.docker.com/compose/compose-file/