前几天借着部署一个小系统的机会尝试了一把docker,把Web应用,数据库,nginx统统都用docker管理。把打包好的代码扔到服务器上一键就部署好了,不需要考虑服务器的环境问题,简直不要太爽。但感觉还是有蛮多坑的,于是做了一个小demo熟悉了一遍。demo在这里,是一个包含了Flask应用,Nginx,MongoDB数据库的Web应用。clone下来直接执行./depoly.sh就能运行了。
接下来总结下其中需要注意的地方。
整体结构
├── db
│ └── dump
├── docker-compose.yml
├── deploy.sh
├── destroy.sh
├── nginx
│ ├── default.conf
│ ├── Dockerfile
│ └── html
│ └── index.html
└── web
├── app
│ ├── app.py
│ ├── __init__.py
├── Dockerfile
├── requirements.txt
└── start.py
MongoDB的镜像不需要做任何修改,db目录下存放的只是需要导入的初始化数据。
nginx目录下存放的是nginx的配置文件、前端代码和对应的Dockerfile文件。
web目录下是Flask app的代码和对应的Dockerfile文件。
docker-compose.yml统一管理需要用到的三个容器。
Dockerfile VS docker-compose
整个项目中就是通过这两个文件操作docker的,刚开始接触可能不是很容易理解他们的差别。比如有时只需要一个Dockerfile,而又有时候只需要一个docker-compose.yml。
Dockerfile 负责构建镜像,因为很多时候从hub上拉去下来的镜像不够符合我们的要求,需要一些定制化的修改。比如用于运行flask应用的是一个python镜像,我们需要给他安装一些python包。
# ./app/Dockerfile
FROMpython:2.7
ADD. /app
WORKDIR/app
RUN pip install -r requirements.txt
在这里我们做的只是把文件导入到docker中,然后安装python依赖包。
docker-compose 有两个比较重要的作用,一个是操作容器,比如暴露端口、挂载目录实现与宿主机文件同步、管理容器网络、执行命令等。这些功能都可以通过docker run后面跟一堆参数实现,但是通过docker-compose实现会清晰和方便很多。另一个作用是统一管理多个容器,一个项目往往需要同时使用多个容器,你肯定希望用一个docker-compose up启动他们而不是一个一个地去docker run。
其实这两者的区别也不是那么清晰,比如上面Dockerfile中导入文件的操作,可以放到compose中变成目录挂载,让宿主机和docker中的文件同步,这样利用Flask的debug模式的热启动,修改宿主机的文件就能立即看到docker中的运行效果了。
docker中的网络
刚开始接触docker的时候可能并不会太注意docker的网络,完全使用默认配置也没什么问题。但是在一个需要多个容器的应用中,不了解docker的网络可能就不知道如何把多个容器串联起来了。
docker服务器启动后会自动创建三个网络:bridge、host、none。
# docker network ls
NETWORK ID NAME DRIVER SCOPE
f3193c939ac5 bridge bridge local
560e7d180738 host host local
e2f3c776cd7c none null local
默认情况下容器启动后都会加入使用bridge模式的bridge网络。在这种模式中,容器使用一个内网IP,并把网关指向docker服务器创建的虚拟网关docker0。
# docker inspect
...
"Gateway": "172.17.0.1",
"IPAddress": "172.17.0.4",
...
通过docker inspect可以看到我们运行的容器IP和它的网关docker0的IP,在宿主机上可以直接ping这个地址。同一个网络中的容器可以相互通信,而不同网络中的容器由于默认不在同一个子网,不能相互通信。
compose file 版本差异
docker compose file已经发布到第三版,第三版主要增加了docker swarm相关的内容。第二版与第三版比较兼容,第一版与第二版的差别比较大,网上的很多示例都是基于第一版,然而第一版在接下来的release中已经准备废弃了,所以在这里记录下几个第一版与第二版差别较大的地方。
容器互联
在前面对网络的介绍中我们知道,每一个容器都有一个IP,容器之间通过IP地址相互通信。但是通常情况下IP地址都是新建容器的时候自动分配,所以用硬编码的方式将其他容器的地址写到代码里是不行的。
在第一版中,通常使用links环境变量注入。使用links命令会将被连接容器的所有环境变量都传递给连接的容器。比如在web应用中连接数据库,就是用link连接数据库容器:
web:
build: .
command: python -u app.py
ports:
- "5000:5000"
volumes:
- .:/todo
links:
- db
db:
image: mongo:3.0.2
然后在web代码中从环境变量中获取数据库地址:
client = MongoClient(
os.environ['DB_PORT_27017_TCP_ADDR'],
27017)
此时web容器中可以获取到所有db容器的环境变量。
在第二版以后,links就是一个即将被弃用的命令了。因为这种注入所有环境变量的方式不太可控,所以建议使用卷共享这种更可控的方式实现环境变量共享。如果容器在同一个网络中,直接使用容器名,就可以实现容器互联。
services:
web:
build: ./web
ports:
- "5678"
volumes:
- /tmp/app
command: /usr/local/bin/gunicorn -w 2 -b :5678 start:app
db:
image: mongo:3.4
container_name: demo_db
from mongoengine import *
connect(db='demo', host='demo_db')
这里直接使用容器名demo_db就可以连接到MongoDB数据库了。
默认网络
在第一版中,使用docker-compose启动的容器都会默认被加入到bridge网络中,与其他使用docker run启动的容器共用一个网络。但在第二版以后,使用docker-compose启动容器时,会创建一个以docker-compose.yml所在文件夹名为前缀的网络。
docker_deploy_demo# docker network ls
NETWORK ID NAME DRIVER SCOPE
4a2bd496dc63 bridge bridge local
c3bc371f694d host host local
10d54b17f5ab none null local
8beddd847aad dockerdeploydemo_default bridge local
比如这里文件夹名是docker_deploy_demo,所以新建的bridge网络是dockerdeploydemo_default。所以使用其他容器连接docker-compose所管理的容器时,需要指定他们所在的网络。比如操作数据库容器导入数据:
docker run --rm -v $('pwd')/db/dump:/backup --net dockerdeploydemo_default mongo bash -c 'mongorestore /backup --host demo_db:27017'
镜像标签
为镜像打个标签可以方便管理自己创建的镜像,在第二版中,如果镜像是由dockerfile生成,那么可以用image为镜像自定义标签。
nginx:
build: ./nginx
image: demo_nginx
ports:
- "80:80"
volumes:
- ./nginx/html:/usr/share/nginx/html
container_name: demo_nginx
但是在第一版中,build和image是不能共存的。
HTTPS
现在使用https已经是Web应用的标配了,在docker中配置https跟真机中并没有太大差别:先获取证书和密钥,再在nginx中配置好证书和密钥。我另外写了一个小demo演示了在docker中配置https,包含了使用letsencrypt的CA认证证书和自签证书。地址在这里。