用 Docker 部署 Python Web Application

Tips: 下面的部分链接中带有小尾巴,谨点

去年的时候从阿里云捻过一次羊毛(云翼计划,如果愿意,可以是用我的阿里云推荐码,z9xy74),如今一年已过,这个服务器也过期了,但是上面跑了一个微信企业号的应用在,由于有一直在使用的需求,不能停止,但是阿里云的羊毛是青岛节点,延迟高,而且现在到期了,默认变成了按流量付费,顿时觉得随时会少一套房子了。于是重新买了一个阿里云的华东节点,准备迁移过来,并且也准备把 DigitalOcean 上的部分应用迁移回国。

Docker 环境的准备

使用阿里云提供的 docker-engine 加速安装,然后替换 Docker Hub Registry 为阿里云提供的加速器。可参考 Docker Cluster with Swarm 这里安装 Docker 和替换 Registry。

Docker 装好后,然后创建一个 docker 用户来专门管理 Docker 和存放一些配置文件。

sudo useradd -m -g docker -s /bin/bash docker  
sudo su docker  
mkdir -p ~/conf ~/logs ~/data ~/source  

镜像的初始化

我的应用是基于 Python 的,使用 Flask 框架,数据库是 MySQL,然后有用到 Redis 做微信 Access Token 的持久化,所以需要先安装 MySQL 和 Redis,最后有用到 Nginx 做代理,这里直接使用的是官方最新镜像

docker pull mysql  
docker pull nginx  
docker pull redis  

因为有用到加速器,所以速度还是杠杠的。

MySQL

然后是分别将镜像跑起来,为了更灵活的使用,我将 MySQl 的配置文件和数据文件通过 volume 挂载到了宿主机上,这样就算容器挂了,数据还是在的。 下面是 MySQL 的相关配置

docker run -d --restart=always \  
    -v /home/docker/data/docker-mysql:/var/lib/mysql \
    -v /home/docker/conf/mysql:/etc/mysql/conf.d:ro \
    --name mysql \
    mysql

上述将 MySQL 的数据文件挂载到了宿主机的 /home/docker/data/docker-mysql 上,并且将 MySQL 的配置文件也挂载到了宿主机上,然后在 /home/docker/conf/mysql 里有设置 MySQL 的默认编码为 utf8 的配置文件 charset.cnf,具体内容如下

[client]
default-character-set = utf8  
[mysqld]
character-set-server = utf8  
[mysql]
default-character-set = utf8  

Redis

Redis 的话,因为只是在内存中持久化,所以对数据的保存没有啥要求,对默认配置也没用做修改,所以直接用 Docker 跑起来就可以了

docker run -d --restart=always \  
    --name redis \
    redis

Web Application

由于用的是 Docker 提供的 link 连接容器的,所以在 Nginx 跑起来之前,还需要 Web 先运行起来。

因为用到了 Docker,这一天然的环境隔离神奇,所以对于 Python 之类的各种库依赖来说,直接一个 Docker 镜像打好,然后就随处都可以正常运行了。我没有将我的镜像放到 Docker Hub 或者其他 Registry 上,而是直接写好 Dockerfile,然后上传到服务器上做镜像。下面是我的 Dockerfile

FROM python:2  
ADD . /code  
WORKDIR /code  
RUN pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt  
CMD gunicorn -c gunicorn.conf wsgi:app  

我的 Web 程序是基于 Flask 的,然后用 gunicorn 使之运行起来,所以我将 gunicorn.conf 也是放在宿主机上,然后通过 volume 挂载到容器内部的。

docker run -d --restart=always \  
    -e MODE=PRODUCTION \
    --name app_name
    -v /home/docker/conf/app_name/gunicorn.conf:/code/gunicorn.conf:ro \
    -v /home/docker/conf/focus/production.py:/code/config/production.py:ro \
    -v /home/docker/logs/focus/gunicorn:/var/log/gunicorn \
    --link redis:redis \
    --link mysql:mysql \
    app_name:tag

其中 /var/log/gunicorn 里有 gunicorn 里写的日志文件,所以也挂载到了宿主机上。然后我的 Web 程序里的配置文件是通过读取环境变量 MODE 来判断是用开发的配置还是正式的配置,其中用到了 Flask 的配置加载,关键代码如下

def load_config():  
    """加载配置类"""
    mode = os.environ.get('MODE')
    logger = logging.getLogger("app")
    try:
        if mode == 'PRODUCTION':
            from .production import ProductionConfig
            enable_pretty_logging({"logging": "info"})
            logger.info(b'load from production')
            return ProductionConfig
        else:
            from .development import DevelopmentConfig
            enable_pretty_logging()
            logger.info(b'load from development')
            return DevelopmentConfig
    except ImportError, e:
        logger.error(e)
        sys.exit(1)

对于容器间的访问,我用的是 Docker 提供的 link,所以通过 docker run 提供的 --link 参数,可以让运行的容器通过一个别名访问另一个容器里的网络,其实是 Docker 写入了容器里的 /etc/hosts 文件里。

Nginx

当一切都运行起来了之后,就是 Nginx 了

docker run -d --restart=always \  
    --name nginx \
    -v /home/docker/logs/nginx:/var/log/nginx \
    -v /home/docker/conf/nginx/nginx.conf:/etc/nginx/nginx.conf:ro \
    -v /home/docker/conf/nginx/conf.d:/etc/nginx/conf.d:ro \
    -p 80:80 \
    -p 433:433 \
    --link app_name:app_name \
    nginx

Nginx 的配置文件和日志也通过 volume 挂载到了宿主机上,这样方便操作,如果配置有了更改,直接 restart 容器即刻,暂时没找到可以不重启容器而直接 reload 配置的方法。

和之前的比较

之前部署 Python 的 Web 应用,我都是 Nginx + gunicorn + supervisor,然后加上 virtualenv 来隔离依赖,现在使用 Docker 部署应用,直接省略掉了 supervisor,因为直接使用 Docker 来保持应用的运行。隔离依赖的话,Docker 比 virtualenv 更好,起码是系统级别的隔离了。

一些问题

用 Docker 提供的 link 会有一些逻辑上的问题,如果当前容器依赖的 link 停止了或者不存在,当前容器也不就不能启动,这样如果 Web 系统复杂起来了,里面存在相互依赖的话,就会出现问题,不过我这太简单,完全不用靠谱 :D。

其实 Docker 有提供 Docker Compose 来管理应用,但是我选择了直接使用 docker run,因为我将 docker run 写到了一个脚本里,所以就不想使用 Docker Compose 来增加更多的东西了。

参考