使用docker一键部署Web应用
前几天借着部署一个小系统的机会尝试了一把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 FROM python: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 <container ID>
...
"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认证证书和自签证书。地址在这里。
请教一下, 如果用flask-sqlalchemy + MySQL的话, app里面应该怎么配置链接数据库?
@冰熊 如果你按照上述那样数据库和web应用分别运行在不同的容器里,而且各容器在同一个子网,那么
SQLALCHEMY_DATABASE_URI = 'mysql://user:pass@{ your_db_container_name }/foo'
hostname直接就用容器名就可以解析到了
@Melw00d 谢谢指导.
还有个问题, 你的demo里面deploy脚本里, 最后一行是以数据库镜像创建一个数据库容器对吧? 参数--net dockerdeploydemo_defaulut是起什么作用?
我跑这个脚本提示说docker: Error response from daemon: network dockerdeploydemo_default not found.
@冰熊 最后一行是创建一个新的mongo容器往之前创建的数据库容器里导入数据,这个容器执行完命令就会被销毁。
你先看看你之前的数据库容器在那个网络里,然后--net指定到对应的网络。
@Melw00d 谢谢
但好像mysql数据库导入的逻辑不太一样, 我再研究研究.
另外, 直接用你的demo来build, 只能进到nginx默认指向静态index, 后端好像还是链接不到
root@vultr:~/docker_demo# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
dda2f075d32e demo_web "/usr/local/bin/gu..." 3 minutes ago Restarting (3) 13 seconds ago demo_web
71cd2c63c9ee mongo:3.4 "docker-entrypoint..." 3 minutes ago Up 3 minutes 27017/tcp demo_db
043215cb6a42 demo_nginx "nginx -g 'daemon ..." 3 minutes ago Up 3 minutes 0.0.0.0:80->80/tcp demo_nginx
@冰熊 我试了下,直接运行./deploy.sh没有问题呀
@Melw00d 我猜还是环境有点不一样. 因为发现在我这里要deploy成功需要把yml最后一行里dockerdeploydemo_default改成dockerdemo_default, 把deploy去掉. 然后后端, 启动gunicorn这里, 可能是环境的路径不太一样导致后端没启动.
@Melw00d 后来我进了web容器里面去看了下log, 发现是 app.py 第20行 item = Note(content=content, created_at=default=datetime.datetime.now()) 这里出了问题, 导致没有启动.
讲道理, 封装好的容器应该不会出这种问题才对吧...
@冰熊 你是直接pull下来的么....git上不是这样的吧.....
@Melw00d 是从github上pull的原版;
我大概找到原因了, 因为一开始没解决shell脚本编码问题, 所以我是手动dock-compose的, 大概导致了容器们处在不同网络?
后来, 脚本set ff=unix之后就可以跑了, index输入框下面有显示日期和hello, 但查询好像还是不行.
脚本导入数据库这步, 按你上面的介绍, 会创建一个以yml所在目录的文件夹命名的网络. Github源码文件夹名是docker_demo, 所以新建的网络名应该是dockerdemo_default, 我这边的情况也是符合这个命名规则, 而不是你源码里的dockerdeploydemo_default... 至于你直接用源码这个网络名可以导入数据库, 唔, 难道是因为docker版本不一样? 我的是18.04-ce.
MySQL的导入方法我也解决好了, MySQL如果是pull官方裸镜像需要新建一个与备份文件一样的database才可以导入, 后续build一个customized的镜像可以省掉这一步.
谢谢指导!