Python项目容器化实践(一) – Docker Compose

前言

作为开发者应该对新技术保持敏锐度,愿意尝试和接受新事物。我从 2013 年开始关注 LXC 和 Docker
,当时我还在做很多运维方面的工作,那会 Docker 刚发布不久,问题很多还不能在生产环境使用;在 2016 年我的书中也提供了 Docker 镜像,读者可以方便的使用这个包含全部代码和相关依赖的环境;而现在 Docker 容器已经被各大互联网公司广泛应用,而且由于 Docker 和 Etcd 等项目还算捧红了 Golang~
借着我个人博客这个小项目,我准备写几篇文章分享一些 Python 项目容器化方面的实践。我的文章里面就不介绍 Docker 已经你为什么应该用它了,网上可能很容易的搜到答案,可以通过延伸阅读链接 1 和 2 获得更多信息,我们直入今天的主题: Docker Compose

什么是 Docker Compose?

很多同学都知道可以用 Docker 创建一个容器,然后用 docker run -it ubuntu:19.04 bash
之类的方式进入容器,就像是在用一个完整的操作系统那样使用它。

事实上除了各种版本的操作系统镜像,官方还维护了很多包含常用软件的镜像,如 Python、MySQL、Redis、Elasticsearch、Nginx 等等,举个例子,我 只是
想在容器里面使用目前最新的 Python3.7,不是用 ubuntu:19.04
这种操作系统镜像,再进入容器安装 Python,可以直接用 Python 对应版本的容器:

❯ docker run -it python:3.7 bash
root@2825696d5639:/# python
Python 3.7.4 (default, Sep 12 2019, 15:40:15)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.

官方维护的相关乳尖镜像列表可以看延伸阅读链接 3 这个项目目录的内容。

在实际的使用中,我们往往需要各种环境,安装各种软件和 Python 依赖,比如想要搭建一个 Web 开发环境,要有数据库、Nginx、Python、Web 框架、缓存等等,当然还是可以基于 ubuntu:19.04
这种操作系统镜像,在 Dockerfile 里面写复杂的逻辑,挨个安装相关的软件和 Python 依赖。不过这样做有一些缺点:

  1. 不利于分享和复用。一般这种方式产生的镜像比较大,且由于里面堆了很多别人用不到的东西,不具备复用的价值
  2. 无法共享数据。应用只能连接到自己内部,如果共享镜像里的数据要配置复杂的端口转发,且容易出错

把全部东西堆到一个容器里面是典型的虚拟机的使用方式,不是 Docker 的正确打开方式

正确的做法是让一个容器做一件事:数据库、Nginx、Python 应用、缓存等等都是独立的容器,分别启动它们,这些容器组成了一个集群,需要某种方法把它们关联起来。
这个关联有一个非常专用、形象的称呼「编排」,我最早了解这个词是通过「Ansbile Playbooks」,而 Docker Compose 大家可以猜到就是负责实现对 Docker 容器集群编排的。
Docker Compose 的官方文档一开头就是对它的定位:

Compose is a tool for defining and running multi-container Docker applications.

可以说,Dockerfile 可以让用户管理一个单独的应用容器,而 Compose 则允许用户在一个模板 (YAML 格式) 中定义一组相关联的应用容器。好了,我们开始体验一下吧。

安装

在Mac下安装Docker自带了
docker-compose` 命令们可以直接使用,否者需要根据官方文档安装它。

实现 lyanna 的 Dockerfile

首先实现这个博客应用的 Dockerfile,看一下最后的全部内容:

FROM python:3.7-alpine AS build
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories \
    && apk update \
    && apk add git gcc musl-dev libffi-dev openssl-dev make
WORKDIR /install
COPY requirements.txt /requirements.txt
RUN pip install -i https://mirrors.ustc.edu.cn/pypi/web/simple -r /requirements.txt \
    && mkdir -p /install/lib/python3.7/site-packages \
    && cp -rp /usr/local/lib/python3.7/site-packages /install/lib/python3.7

FROM python:3.7-alpine
COPY --from=build /install/lib /usr/local/lib
COPY --from=build /install/src /app/src
WORKDIR /app
COPY . /app

这里要解释的很多,我按行来说:

  1. 第 1 行。 python:3.7-alpine
    是一个在 Alpine 系统下 Python3.7 镜像, Alpine
    的优势是「系统的体积小」(系统镜像约 5 MB,而 Ubuntu 系列镜像接近 200 MB,可见一斑)、「运行时资源消耗低」和「提供包管理工具 apk
    ,包管理机制完善」等。我是很推荐使用 Alpine 替代 Ubuntu 之类系统做为基础镜像环境的。
  2. 第 2-4 行。是为了修改 Alpine 使用国内源,可以明显加快软件下载。
  3. 第 5 行。切换工作目录到 /install
  4. 第 6 行。把本地的 requirements.txt
    拷贝进容器
  5. 第 7-9 行。使用国内源安装项目依赖,并且把下载的包拷贝到 /install/lib/python3.7
    。我都是用 &&
    把同类操作放在一层,可以显著减少容器大小。
  6. 第 11 行。如果你之前接触过 Dockerfile 可能会疑惑这个文件里面有 2 句 FROM,这么做是 Docker 的 多阶段构建
    ,如果不使用分段构建,会带来「镜像层次多,镜像体积较大,部署时间长」等问题,现在镜像体积会明显减少。
  7. 第 12-13 行。把从 build 容器下载的内容直接复制进来
  8. 第 14 行。切换工作目录到 /app
  9. 第 15 行。把应用代码拷贝进 /app

有些同学可能还见过这样的代码:

EXPOSE 5000
CMD python app.py

之前说过,Dockerfile 是用来管理单个应用容器的,单个应用是需要这样启动应用,然后暴露对应端口,但我们很快就要用 Compose 来管理,所以是不需要的。
接着我们构建一下这个容器,再对比一下几个容易的大小:

❯ docker build -t lyanna-app .

❯ docker images |egrep "lyanna-app|3.7"
lyanna-app          latest              f8908aadc20e        8 minutes ago       210MB
python              3.7                 02d2bb146b3b        9 days ago          918MB
python              3.7-alpine          39fb80313465        3 weeks ago         98.7MB

可以看到在 alpine 系统构建的 Python3.7 (python:3.7-alpine) 只是 Debian 版本 (python:3.7) 的十分之一,而我们在安装那么多系统包 (git、gcc、make 和 openssl-dev 等) 和依赖 (requirements.txt) 之后才 210MB。

让 lyanna 使用 Compose

之前就有对 lyanna 感兴趣的同学由于对这一套环境不熟悉而放弃了体验 lyanna,这次我借机构建一套包含 lyanna 所需全部环境的容器集群。这个容器集群包含:

  1. db。存放博客、用户、表态、评论等数据。
  2. redis。存放博客文章内容以及作为消息代理。
  3. memcached。缓存。
  4. web。lyanna 应用。

Compose 是通过 docker-compose.yml
这个模板文件来控制编排的:

version: '3'
services:
  db:
    image: mysql
    restart: always
    environment:
      MYSQL_DATABASE: 'test'
      MYSQL_USER: 'root'
      MYSQL_PASSWORD: ''
      MYSQL_ROOT_PASSWORD: ''
      MYSQL_ALLOW_EMPTY_PASSWORD: 'true'
    ports:
      - '3306:3306'
    volumes:
      - my-datavolume:/var/lib/mysql
    networks:
      - app-network
  redis:
    image: redis:alpine
    networks:
      - app-network
  memcached:
    image: memcached:1.5-alpine
    networks:
      - app-network
  web:
    networks:
      - app-network
    build: .
    ports:
      - '8000:8000'
    expose:
      - '8000'
    volumes:
      - .:/app
      - ./local_settings.py.tmpl:/app/local_settings.py
    depends_on:
      - db
      - redis
      - memcached
    environment:
      PYTHONPATH: $PYTHONPATH:/usr/local/src/aiomcache:/usr/local/src/tortoise:/usr/local/src/arq:/usr/local/src
    command: sh -c 'sleep 5 && ./setup.sh && python app.py'
volumes:
  my-datavolume:
networks:
  app-network:
    driver: bridge

这样用户可以一个命令就启动这个环境 (加 – d 可以用 daemon 的方式启动):

❯ docker-compose up
Starting lyanna_memcached_1 ... done
Starting lyanna_db_1        ... done
Starting lyanna_redis_1     ... done
Starting lyanna_web_1       ... done
Attaching to lyanna_redis_1, lyanna_memcached_1, lyanna_db_1, lyanna_web_1
...

然后访问 http://localhost:8000/
就能看到博客效果了,由于把 lyanna 代码目录挂载到了容器中,且 python app.py
启动了 DEBUG 模式,所以在本机的代码修改可以直接在容器里面生效,可以用来做本地后端开发。
具体的键及其意义以及可选值需要通过官方文档了解,我只介绍这个例子中出现的这些键:

networks: - app-network
volumes

在这个模板中,有一个地方我需要重点说明:

web:
  volumes:
    - ./local_settings.py.tmpl:/app/local_settings.py  # :point_left:

lyanna 支持使用 local_settings.py
覆盖默认的 config.py 里面的设置,我为什么要在 Docker 里面使用本地设置呢?先看一下这个默认设置:

❯ cat local_settings.py.tmpl
DEBUG = True
REDIS_URL = 'redis://redis:6379'
MEMCACHED_HOST = 'memcached'
DB_URL = 'mysql://root:@db:3306/test?charset=utf8'

这里面特意设置了 db、redis 和 memcached 的地址,注意它们的主机名,不是原来的 localhost
,而是用了前面 docker-compose.yml
里面定义的服务的名字。这是因为 在桥接模式 (默认) 下,每个容器都有自己的 IP,容器之间通讯需要使用服务名字
,因为此时 localhost 指的是容器自己的本地地址,而访问不到其他服务了。这个地方我觉得太重要了,当时花了很多时间去解决多容器通讯的问题,但是无论是官方文档还是技术文章鲜少提到这个地方。

Compose 只针对开发和测试环境

Compose 非常适合构建开发和测试环境,但如果你想在生产中使用你的容器,应该选择 Kubernetes 来编排容器,原因之后会聊!~

延伸阅读