/ Docker

用 Docker 部署 Python Web Application

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

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

# Docker 环境的准备 使用阿里云提供的 [docker-engine](http://mirrors.aliyun.com/help/docker-engine) 加速安装,然后替换 Docker Hub Registry 为阿里云提供的加速器。可参考 [Docker Cluster with Swarm](https://hui.lu/docker-cluster/#make-ubuntu-docker-base-box) 这里安装 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 做代理,这里直接使用的是官方最新镜像 ```bash docker pull mysql docker pull nginx docker pull redis ``` 因为有用到加速器,所以速度还是杠杠的。
## MySQL 然后是分别将镜像跑起来,为了更灵活的使用,我将 MySQl 的配置文件和数据文件通过 volume 挂载到了宿主机上,这样就算容器挂了,数据还是在的。 下面是 MySQL 的相关配置 ```bash 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`,具体内容如下 ```ini [client] default-character-set = utf8 [mysqld] character-set-server = utf8 [mysql] default-character-set = utf8 ```
## Redis Redis 的话,因为只是在内存中持久化,所以对数据的保存没有啥要求,对默认配置也没用做修改,所以直接用 Docker 跑起来就可以了 ```bash 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 了 ```bash 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 来增加更多的东西了。

# 参考 * http://hub.docker.com/_/nginx/ * http://hub.docker.com/_/mysql/