使用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

但是在第一版中,buildimage是不能共存的。

HTTPS

现在使用https已经是Web应用的标配了,在docker中配置https跟真机中并没有太大差别:先获取证书和密钥,再在nginx中配置好证书和密钥。我另外写了一个小demo演示了在docker中配置https,包含了使用letsencrypt的CA认证证书和自签证书。地址在这里

Comments
Write a Comment
  • 冰熊 reply

    请教一下, 如果用flask-sqlalchemy + MySQL的话, app里面应该怎么配置链接数据库?

    • Melw00d reply

      @冰熊 如果你按照上述那样数据库和web应用分别运行在不同的容器里,而且各容器在同一个子网,那么

      SQLALCHEMY_DATABASE_URI = 'mysql://user:pass@{ your_db_container_name }/foo'

      hostname直接就用容器名就可以解析到了

      • 冰熊 reply

        @Melw00d 谢谢指导.

        还有个问题, 你的demo里面deploy脚本里, 最后一行是以数据库镜像创建一个数据库容器对吧? 参数--net dockerdeploydemo_defaulut是起什么作用?

        我跑这个脚本提示说docker: Error response from daemon: network dockerdeploydemo_default not found.

        • Melw00d reply

          @冰熊 最后一行是创建一个新的mongo容器往之前创建的数据库容器里导入数据,这个容器执行完命令就会被销毁。

          你先看看你之前的数据库容器在那个网络里,然后--net指定到对应的网络。

          • 冰熊 reply

            @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

            • Melw00d reply

              @冰熊 我试了下,直接运行./deploy.sh没有问题呀

              • 冰熊 reply

                @Melw00d 我猜还是环境有点不一样. 因为发现在我这里要deploy成功需要把yml最后一行里dockerdeploydemo_default改成dockerdemo_default, 把deploy去掉. 然后后端, 启动gunicorn这里, 可能是环境的路径不太一样导致后端没启动.

              • 冰熊 reply

                @Melw00d 后来我进了web容器里面去看了下log, 发现是 app.py 第20行 item = Note(content=content, created_at=default=datetime.datetime.now()) 这里出了问题, 导致没有启动.

                讲道理, 封装好的容器应该不会出这种问题才对吧...

                • Melw00d reply

                  @冰熊 你是直接pull下来的么....git上不是这样的吧.....

                  • 冰熊 reply

                    @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的镜像可以省掉这一步.

                    谢谢指导!