让自己的Python应用容器化

本文总阅读量

现在云原生越来越流行, 容器化势在必行, 所以也要学会如何把自己编写的Python Web应用编写为一个Docker容器来运行, 本文示例以starlette为Python Web框架, 创建一个简单的演示项目, 并且带有后台开发常见3件套nginx, mysql, redis做为例子, 一步一步编介绍如何写成一个容器, 并在生产环境运行.

注: 2021-02-07增加了示例代码, 以及准备写docker swarm章节

1.创建项目

首先创建一个后台项目以及依赖:

1
2
3
4
5
6
7
8
9
10
11
12
➜  example_python git:(master) mkdir python_on_docker          
➜ example_python git:(master) cd python_on_docker
# 一个项目配套一个虚拟环境, 如果熟悉, 建议用porety来处理项目的python和venv配套环境问题, 这里为了演示方便, 使用了venv
➜ python_on_docker git:(master) python3.7 -m venv venv
➜ python_on_docker git:(master) source venv/bin/activate
➜ python_on_docker git:(master) touch __init__.py # 每个Python项目要确保有一个__init__.py文件

# 可以看到多了个venv的环境, 目前已经且到venv
(venv) ➜ python_on_docker git:(master) pip install starlette aiomysql aioredis uvicorn
(venv) ➜ python_on_docker git:(master) pip install cryptography # aiomysql需要该模块提供加密方法
# 生成pip安装的依赖文件
(venv) ➜ python_on_docker git:(master) python -m pip freeze > requirements.txt

之后创建项目文件example.py, 里面包含3个接口, 一个用于测试服务是否正常, 一个是mysql相关调用, 一个是redis相关调用, 示例代码如下(源码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
from typing import Optional, Tuple

import aiomysql
import aioredis
from starlette.applications import Starlette
from starlette.config import Config
from starlette.requests import Request
from starlette.responses import JSONResponse, PlainTextResponse
from starlette.routing import Route


config: Config = Config(".env")
mysql_pool: Optional[aiomysql.Pool] = None
redis: Optional[aioredis.Redis] = None


async def on_start_up():
global mysql_pool
global redis

mysql_pool = await aiomysql.create_pool(
host=config("MYSQL_HOST"),
port=config("MYSQL_PORT", cast=int),
user=config("MYSQL_USER"),
password=config("MYSQL_PW"),
db=config("MYSQL_DB"),
)
redis = aioredis.Redis(
await aioredis.create_redis_pool(
config("REDIS_URL"),
minsize=config("REDIS_POOL_MINSIZE", cast=int),
maxsize=config("REDIS_POOL_MAXSIZE", cast=int),
encoding=config("REDIS_ENCODING")
)
)


async def on_shutdown():
await mysql_pool.wait_closed()
await redis.wait_closed()


def hello_word(request: Request) -> PlainTextResponse:
return PlainTextResponse("Hello Word!")


async def mysql_demo(request: Request) -> JSONResponse:
count: int = int(request.query_params.get("count", "0"))
async with mysql_pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute("SELECT %s;", (count, ))
mysql_result_tuple: Tuple[int] = await cur.fetchone()
return JSONResponse({"result": mysql_result_tuple})


async def redis_demo(request: Request) -> JSONResponse:
count: int = int(request.query_params.get("count", "0"))
key: str = request.query_params.get("key")
if not key:
return JSONResponse("key is empty")
result: int = await redis.incrby(key, count)
await redis.expire(key, 60)
return JSONResponse({"count": result})


app: Starlette = Starlette(
routes=[
Route('/', hello_word),
Route('/mysql', mysql_demo),
Route('/redis', redis_demo)
],
on_startup=[on_start_up],
on_shutdown=[on_shutdown]
)

项目创建完毕, 再创建一个配置文件.env:

1
2
3
4
5
6
7
8
9
10
11
12
# 按自己的配置信息更改配置
MYSQL_DB="mysql"
MYSQL_HOST="127.0.0.1"
MYSQL_PORT="3306"
MYSQL_USER="root"
MYSQL_PW=""

REDIS_URL="redis://localhost"
REDIS_POOL_MINSIZE=1
REDIS_POOL_MAXSIZE=10
REDIS_ENCODING="utf-8"

到现在为止, 目录里只有example.py的应用文件, .env配置文件以及requirements.txt的依赖文件, 开始启动应用查看应用是否能正常启动(注意修改mysql和redis的配置文件, 假设已经在本地安装好mysql和redis):

1
2
3
4
5
6
7
# 使用python -m uvicorn可以防止调用到外部的uvicorn
python -m uvicorn example:app
# 以下为终端输出
INFO: Started server process [4616]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

可以看到我们的服务已经启动并监听本机的8000端口,接下来测试接口可否正常使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
➜  curl http://127.0.0.1:8000
Hello Word!% ➜ curl http://127.0.0.1:8000/mysql
{"result":[0]}
➜ curl http://127.0.0.1:8000/mysql\?count\=10
{"result":[10]}
➜ curl http://127.0.0.1:8000/mysql\?count\=50
{"result":[50]}
➜ curl http://127.0.0.1:8000/redis\?key\=test
{"count":0}
➜ curl http://127.0.0.1:8000/redis\?key\=test\&count\=2
{"count":2}
➜ curl http://127.0.0.1:8000/redis\?key\=test\&count\=2
{"count":4}
➜ curl http://127.0.0.1:8000/redis\?key\=test\&count\=2
{"count":6}
➜ curl http://127.0.0.1:8000/redis\?key\=test\&count\=2
{"count":8}

通过输出可以发现, 我们的测试结果正常, 接口可以正常使用,前菜到此结束, 开始利用docker部署Python Web应用之旅.

2.为项目创建镜像并运行

目前我们还没碰过Docker, 从这里开始, 就开始使用Docker了, 但在使用之前要确保自己安装了Docker, 每个平台都有不同的安装方法且资料很多, 这里就不做描述了.

项目编写好了, Docker也安装好了就可以开始创建自己的镜像, 在Docker中需要通过一个Dockerfile来帮忙告诉Docker怎么制作一个镜像, Dockerfile主要包括两个用途. 一个是对当前应用的描述. 一个是指导Docker完成应用的容器化(创建一个包含当前应用的镜像),Dockerfile能实现开发和部署两个过程的无缝切换.同时Dockerfile还能帮助新手快速熟悉这个项目.Dockerfile对当前的应用及其依赖有一个清晰准确的描述,并且非常容易阅读和理解.因此,要像重视你的代码一样重视这个文件,并且将它纳入到源控制系统当中.

我们项目的Dockerfile如下(源码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 拉取python的基础镜像, 具体使用python -V查看刚才自己是哪个版本的
FROM python:3.7.4-alpine
# 设置当前镜像的维护者
LABEL maintainer="so1nxxxx@gmail.com"
# 设置工作目录
WORKDIR /data/app
# 复制本地依赖, 每个COPY会新建一个镜像层
COPY . .

# 设置环境变量
# 不要生成pyc文件
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# 安装依赖, 由于aioredis依赖的hiredis以及cryptography需要编译, 所以这里需要add这些包, 容器就变得很大...
RUN apk add --update gcc musl-dev python3-dev libffi-dev openssl-dev build-base && pip install --upgrade pip && pip install -r requirements.txt

# 指明监听的端口
EXPOSE 8080

# 运行的命令
CMD ["uvicorn", "--host", "0.0.0.0", "example:app"]

Dockerfile虽然命令有点多, 但是不复杂,理解后就会发现这些命令可读性很高:

  • FROM: 每个Dockerfile文件第一行都是FROM指令.FROM指令指定的镜像,都会作为当前镜像的一个基础镜像层,当前应用的剩余内容会作为新增镜像层添加到基础镜像层之上。这里我们使用python:xxx-alpine的镜像都是官方的镜像, 这个镜像的Python是建立在一个alpine Linux的镜像上面, alpine Linux体积非常小, 不过麻雀虽小, 但五脏俱全.使用FROM指令引用官方基础镜像是一个很好的习惯,这是因为官方的镜像通常会遵循一些最佳实践,并且能帮助使用者规避一些已知的问题。除此之外,使用FROM的时候选择一个相对较小的镜像文件通常也能避免一些潜在的问题.

  • LABEL: Dockerfile中通过标签(LABLE)方式指定了当前镜像的维护者.每个标签其实是一个键值对(Key-Value),在一个镜像当中可以通过增加标签的方式来为镜像添加自定义元数据.

  • WORKDIR: 指明当前的工作目录

  • COPY: 复制本地目录, 令将应用相关文件从构建上下文复制到了当前镜像中,并且新建一个镜像层来存储.

  • ENV: 设置环境变量

  • RUN: 执行命令, RUN指令会在FROM指定的alpine基础镜像之上,新建一个镜像层来存储这些安装内容.

  • EXPOSE: 指明监听的端口, 实际上并没有任何作用

  • CMD: 启动时运行的命令

    配置打开都是这些, 在COPYRUN命令中, 我都说到了新建了一个镜像层, 那怎么区分哪些命令就会新增一个镜像层呢?关于如何区分命令是否会新建镜像层,一个基本的原则是,如果指令的作用是向镜像中增添新的文件或者程序,那么这条指令就会新建镜像层;如果只是告诉Docker如何完成构建或者如何运行应用程序,那么就只会增加镜像的元数据.
    要注意的是, Docker镜像应该尽量的少, 镜像层越多, 意味着更难用和更慢, 即使本地目录一样, 不同的Dockerfile写法都会导致镜像层数量的不同,每一个RUN指令基本都会新增一个镜像层,通过使用&&连接多个命令以及使用反斜杠(\)换行的方法,将多个命令包含在一个RUN指令中, 可以减少镜像层的产生.不过, 有些时候要把RUN拆分的, 因为
    docker自带了一个缓存机制, 如果这个RUN在缓存中时, docker会构建的比较快, 而在把所有RUN合并到同一条时, 基本上很难命中缓存了.不过还需要注意的是, docker在执行到第一句不命中缓存的命令后, 后面的所有命令是都不会通过缓存构建的.

在构建镜像之前, 我们先检查我们的目录:

1
2
(venv) ➜  python_on_docker git:(master) ls -a
Dockerfile example.py requirements.txt __pycache__ venv .env

发现有__pycache__venv文件, 构建的时候肯定是不想把他们带进去的, Docker提供了跟gitignore的机制, 我们通过.dockerignore文件编写我们要忽略的文件即可:

1
2
__pycache__/
.venv

Dockerfile和.dockerignore文件创建完了, 可以通过以下的命令开始构建自己的镜像了:

1
2
# -t后面是标签, 可以自己填写, .代表当前目录
➜ docker image build -t app:latest .

构建完查看当前镜像

1
2
3
4
# 查看当前的镜像
➜ version_1 git:(master) docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 3351ee7a79ac About a minute ago 435MB

通过docker image inspect xxx可以查看镜像的有多少层, 有哪些卷和配置信息, 由于返回的数据会比较多, 这里就不展示了.
通过命令查看我们的镜像编译情况, 可以看到, apk ...那里占用的空间最大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
➜  version_1 git:(master) docker history app
IMAGE CREATED CREATED BY SIZE COMMENT
3351ee7a79ac About a minute ago /bin/sh -c #(nop) CMD ["uvicorn" "--host" "… 0B
f7fedcb216b0 About a minute ago /bin/sh -c #(nop) EXPOSE 8080 0B
190fd056b947 About a minute ago /bin/sh -c apk add --update gcc musl-dev pyt… 313MB
66901ff8b9d4 5 minutes ago /bin/sh -c #(nop) ENV PYTHONUNBUFFERED=1 0B
7e85b2fa504e 5 minutes ago /bin/sh -c #(nop) ENV PYTHONDONTWRITEBYTECO… 0B
a2714bff8c12 5 minutes ago /bin/sh -c #(nop) COPY dir:26dace857b0be9773… 23.7MB
dc4d69bd98e5 5 minutes ago /bin/sh -c #(nop) WORKDIR /data/app 0B
db1533598434 5 minutes ago /bin/sh -c #(nop) LABEL maintainer=so1nxxxx… 0B
f309434dea3a 16 months ago /bin/sh -c #(nop) CMD ["python3"] 0B
<missing> 16 months ago /bin/sh -c set -ex; wget -O get-pip.py "$P… 6.24MB
<missing> 16 months ago /bin/sh -c #(nop) ENV PYTHON_GET_PIP_SHA256… 0B
<missing> 16 months ago /bin/sh -c #(nop) ENV PYTHON_GET_PIP_URL=ht… 0B
<missing> 16 months ago /bin/sh -c #(nop) ENV PYTHON_PIP_VERSION=19… 0B
<missing> 17 months ago /bin/sh -c cd /usr/local/bin && ln -s idle3… 32B
<missing> 17 months ago /bin/sh -c set -ex && apk add --no-cache --… 86.4MB
<missing> 17 months ago /bin/sh -c #(nop) ENV PYTHON_VERSION=3.7.4 0B
<missing> 17 months ago /bin/sh -c #(nop) ENV GPG_KEY=0D96DF4D4110E… 0B
<missing> 17 months ago /bin/sh -c apk add --no-cache ca-certificates 551kB
<missing> 17 months ago /bin/sh -c #(nop) ENV LANG=C.UTF-8 0B
<missing> 17 months ago /bin/sh -c #(nop) ENV PATH=/usr/local/bin:/… 0B
<missing> 17 months ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 17 months ago /bin/sh -c #(nop) ADD file:fe64057fbb83dccb9… 5.58MB

在上面说过,在RUN的时候需要安装一些编译依赖, 才能安装我们的想要的库, 但这些依赖都非常大, docker针对这种情况, 提供了多阶段构建的功能(还有建造者模式, 但不如多阶段构建),
多阶段构建方式使用一个Dockerfile,其中包含多个FROM指令。每一个FROM指令都是一个新的构建阶段(Build Stage),并且可以方便地复制之前阶段的构件.通过多阶段构建的Dockerfile如下(源码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#####################
# 编译依赖的配置文件#
#####################
FROM python:3.7.4-alpine as builder
# 设置工作目录
WORKDIR /data/app
# 复制本地依赖
COPY . .

# 设置环境变量
# 不要生成pyc文件
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# 安装依赖并编译文件到/data/python_wheels
RUN apk add --update gcc musl-dev python3-dev libffi-dev openssl-dev build-base && pip install --upgrade pip && pip wheel --no-cache-dir --no-deps --wheel-dir /data/python_wheels -r requirements.txt

#####################
# 线上使用的配置文件#
#####################

# 拉取python的基础镜像, 具体使用python -V查看刚才自己是哪个版本的
FROM python:3.7.4-alpine
# 设置当前镜像的维护者
LABEL maintainer="so1nxxxx@gmail.com"
# 设置工作目录
WORKDIR /data/app
# 复制本地依赖
COPY . .

COPY --from=builder /data/python_wheels /data/python_wheels
RUN pip install --no-cache /data/python_wheels/*

# 指明监听的端口
EXPOSE 8080

# 运行的命令
CMD ["uvicorn", "--host", "0.0.0.0", "example:app"]
```
`Dockerfile`文件中有两个`FROM`, 一个`FROM`代表一个单独的构建阶段,第一格阶段是根据当前的`Python`环境, 并安装编译需要的依赖, 然后根据`requirements`生成`Python`的 wheel文件, 并把他指向`/data/python_wheels`里.第二个`FROM`还是跟刚才的一样, 直到`COPY`语句,这里是一个`COPY --from`指令,它从之前的阶段构建的镜像中仅复制生产环境相关的应用代码,而不会复制生产环境不需要的构件, 这个语句的意思是从builder构建阶段的`/data/python_wheels`复制到当前构建阶段的`/data/python_wheels`.接下来`RUN`语句也发生改变, 由于在第一阶段已经编译好了依赖, 这里直接使用依赖进行安装即可. 后面的就跟前面一样, 没有什么变化, `Dkckerfile`文件编写好了, 开始构建自己的镜像:
```bash
# -t后面是标签, 可以自己填写, .代表当前目录
➜ docker image build -t app_1:latest .

查看构建完的镜像, 镜像的大小已经减少很多了, 差不多只剩4分之一,完美! 如果想让镜像更小一点, 在使用bliud命令时, 可以添加--squash命令, 这样docker在build的时候会把所有镜像层合并为一个, 但这也是有缺点的, 合并的镜像层无法共享镜像层,而且push和pull的开销会变得很大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
➜  version_2 git:(master) docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
app_1 latest a71a4a7db157 7 seconds ago 116MB
app latest 3351ee7a79ac 9 minutes ago 435MB


➜ version_2 git:(master) docker history app_1
IMAGE CREATED CREATED BY SIZE COMMENT
a71a4a7db157 43 seconds ago /bin/sh -c #(nop) CMD ["uvicorn" "--host" "… 0B
d4d38b71a1ba 43 seconds ago /bin/sh -c #(nop) EXPOSE 8080 0B
5fb10c8afea8 43 seconds ago /bin/sh -c pip install --no-cache /data/pyth… 15.3MB
e454bbe54adb 46 seconds ago /bin/sh -c #(nop) COPY dir:ff6195d46738a79a1… 2.13MB
d70a8a552490 46 seconds ago /bin/sh -c #(nop) COPY dir:fbe9ac8ac1636d3d7… 3.63kB
dc4d69bd98e5 14 minutes ago /bin/sh -c #(nop) WORKDIR /data/app 0B
db1533598434 14 minutes ago /bin/sh -c #(nop) LABEL maintainer=so1nxxxx… 0B
f309434dea3a 16 months ago /bin/sh -c #(nop) CMD ["python3"] 0B
<missing> 16 months ago /bin/sh -c set -ex; wget -O get-pip.py "$P… 6.24MB
<missing> 16 months ago /bin/sh -c #(nop) ENV PYTHON_GET_PIP_SHA256… 0B
<missing> 16 months ago /bin/sh -c #(nop) ENV PYTHON_GET_PIP_URL=ht… 0B
<missing> 16 months ago /bin/sh -c #(nop) ENV PYTHON_PIP_VERSION=19… 0B
<missing> 17 months ago /bin/sh -c cd /usr/local/bin && ln -s idle3… 32B
<missing> 17 months ago /bin/sh -c set -ex && apk add --no-cache --… 86.4MB
<missing> 17 months ago /bin/sh -c #(nop) ENV PYTHON_VERSION=3.7.4 0B
<missing> 17 months ago /bin/sh -c #(nop) ENV GPG_KEY=0D96DF4D4110E… 0B
<missing> 17 months ago /bin/sh -c apk add --no-cache ca-certificates 551kB
<missing> 17 months ago /bin/sh -c #(nop) ENV LANG=C.UTF-8 0B
<missing> 17 months ago /bin/sh -c #(nop) ENV PATH=/usr/local/bin:/… 0B
<missing> 17 months ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 17 months ago /bin/sh -c #(nop) ADD file:fe64057fbb83dccb9… 5.58MB

镜像创建完了, 开始启动容器:

1
2
# 执行容器运行命令, --name参数可以自己定, -p 参数指定容器的端口(第二个)绑定到本机的端口(第一格), 最后一个参数为image id每次生成的镜像都不同
➜ docker container run -d --name docker_app_1 -p 8000:8000 app_1

查看容器运行情况, 发现是运行失败, 只能通过运行日志查看为什么失败

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 启动后查看容器启动失败, 很正常, 配置用的是`127.0.0.1`而容器没有安装mysql和redis,肯定是连不上的 
# 查看容器状态
➜ version_2 git:(master) docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3070cf77c951 app_1 "uvicorn --host 0.0.…" 7 seconds ago Exited (0) 6 seconds ago docker_app_1
# 查看运行日志
➜ version_2 git:(master) docker logs -f -t --tail 10 docker_app_1
2021-02-07T09:01:16.062239955Z await pool._fill_free_pool(False)
2021-02-07T09:01:16.062241993Z File "/usr/local/lib/python3.7/site-packages/aiomysql/pool.py", line 168, in _fill_free_pool
2021-02-07T09:01:16.062250734Z **self._conn_kwargs)
2021-02-07T09:01:16.062253106Z File "/usr/local/lib/python3.7/site-packages/aiomysql/connection.py", line 75, in _connect
2021-02-07T09:01:16.062255305Z await conn._connect()
2021-02-07T09:01:16.062257318Z File "/usr/local/lib/python3.7/site-packages/aiomysql/connection.py", line 523, in _connect
2021-02-07T09:01:16.062259455Z self._host) from e
2021-02-07T09:01:16.062275244Z pymysql.err.OperationalError: (2003, "Can't connect to MySQL server on '127.0.0.1'")
2021-02-07T09:01:16.062277250Z
2021-02-07T09:01:16.062279127Z ERROR: Application startup failed. Exiting.

可以看到, 由于连不上127.0.0.1导致报错了, 可是本机上面已经安装了Mysql啊, 为什么连不上呢? 那是因为docker共有3个网络模式, Docker容器运行的时候有host、bridge、none三种网络可供配置。

  • bridge bridge是默认选项. bridge即桥接网络,以桥接模式连接到宿主机, 这时候容器内的应用访问127.0.0.1是指容器本身的网络, 通过ifconfig可以看到有个类似于docker0的端口, 该模式下如果要连接到宿主机的网络, 只能把ip改为本机的局域网ip;

  • host host是宿主网络,即与宿主机共用网络, 跟平时一样正常使用即可, 网络性能也是最好的, 如果在使用bridge模式时发现有网络瓶颈, 或者应用对网络延迟和并发有极高的要求, 记得切为host网络模式;

  • none则表示无网络,容器将无法联网.

    所以我们只要把网络模式改为host, 就能解决连不上的问题了. 只要通过去掉-p 8000:8000选项, 增加--net=host选项, (如果旧容器存在, 记得删除掉不用占用空间):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # 启动容器
    ➜ version_2 git:(master) docker container run -d --name docker_app_1 --net=host app_1
    cd1ea057cdb6ec6ee3917d13f9c3c55db2a2949e409716d1dbb86f34bb1356e5

    # 查看启动日志, 正常!
    ➜ version_2 git:(master) docker logs -f -t --tail 10 docker_app_1
    2021-02-07T09:06:35.403888447Z INFO: Started server process [1]
    2021-02-07T09:06:35.403903761Z INFO: Waiting for application startup.
    2021-02-07T09:06:35.437776480Z INFO: Application startup complete.
    2021-02-07T09:06:35.438466743Z INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

    # 调用接口发现启动成功
    ➜ curl 127.0.0.1:8000
    127.0.0.1, Hello Word!

    则此, 我们终于把python的web应用构建成镜像, 并正常启动镜像了

3.单引擎模式部署和管理多容器应用

如果用过Ansible, 就知道会有一个palybook的yaml配置文件, 机器上安装什么应用都是通过palybook控制的, 而这一切都是系统层的. 懂了如何制作容器之后, 就可以开始准备成为Yaml工程师了, 上面我们只编译一个Python应用的镜像, 然后连接了本地的MysqlRedis服务, 我们也可以把MysqlRedis容器化, 不过这时候要一个一个Dockerfile来配置并使用的话就太麻烦了.如果有一个像Dockerfile的文件, 文件写了如何安装这3个镜像, 然后我们执行一个命令就可以把这套环境安装到服务器上, 那就很方便了.在DockerDocker Compose提供了这个服务.

Docker Compose通过一个声明式的配置文件描述整个应用,从而使用一条命令完成部署.应用部署成功后,还可以通过一系列简单的命令实现对其完整声明周期的管理.甚至,配置文件还可以置于版本控制系统中进行存储和管理, 这个工具会跟docker一起安装.

这次相比于之前的服务多了个nginx, 现在目录创建nginx文件, 编写nginx.conf(源码):

Conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
upstream app_server {
server app:8000;
}

server {

listen 80;

location / {
proxy_pass http://app_server;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}

}

可以看到upstream里面有个奇怪的app:8000,先不管, 再创建Dockerfile(源码):

1
2
3
4
FROM nginx:1.19.0-alpine

RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d

现在开始编写我们的docker-compose.yml文件, 假设我们现在的单服务器需要有Python的Web服务, Nginx, Redis, Mysql(源码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# version是必须指定的,而且总是位于文件的第一行。它定义了Compose文件格式(主要是API)的版本。建议使用最新版本。
version: "3.5"
# 用于定义不同的应用服务
services:
redis:
# 基于redis:alpine镜像启动一个独立的名为redis的容器。
image: "redis:alpine"
# 使得Docker可以将服务连接到指定的网络上。这个网络应该是已经存在的,或者是在networks一级key中定义的网络。
networks:
- local-net
# 指定Docker将容器内(-target)的6379端口映射到主机(published)的63790端口。
ports:
- target: 6379
published: 63790
mysql:
image: mysql
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci #设置utf8字符集
restart: always
networks:
local-net:
environment:
# 通过环境变量设置 mysql需要的用户名和密码等
MYSQL_DATABASE: 'test'
MYSQL_USER: 'root'
MYSQL_PASSWORD: ''
MYSQL_ROOT_PASSWORD: ''
MYSQL_ALLOW_EMPTY_PASSWORD: 'true'

# 指定Docker将容器内(-target)的3306端口映射到主机(published)的33060端口。
ports:
- target: 3306
published: 33060
volumes:
- type: volume
source: local-vol
target: /example_volumes
app:
# 指定Docker基于当前目录(.)下Dockerfile中定义的指令来构建一个新镜像。该镜像会被用于启动该服务的容器。
build: .
# 指定Docker在容器中执行的命令, 我们的Dockerfile中已经有了
# command:
# 指定Docker将容器内(-target)的8000端口映射到主机(published)的8000端口。
ports:
- target: 8000
published: 8000
networks:
- local-net
# 挂载到本地的卷
volumes:
- type: volume
source: local-vol
target: /example_volumes
# 声明需要依赖上面的服务
depends_on:
- mysql
- redis
nginx:
build: ./nginx

# 指定Docker将容器内(-target)的80端口映射到主机(published)的8001端口。
networks:
- local-net
ports:
- target: 80
published: 8001
depends_on:
- app



# networks用于指引Docker创建新的网络。默认情况下,Docker Compose会创建bridge网络。这是一种单主机网络,只能够实现同一主机上容器的连接。当然,也可以使用driver属性来指定不同的网络类型。
networks:
local-net:
driver: bridge

volumes:
local-vol:

文件创建完了, 虽然容器都会将端口映射到主机上面, 但是我们全部都配置了一个local-net网络, 它是bridge莫得的网络, 容器里面访问127.0.0.1是访问不到其他容器了. 不过由于local-net网络, 我们可以通过服务名直接访问到对应的容器(国内的教程很多都没说, 巨坑), 所以上面的nginx.conf配置才有app:8000这个选项, 指的是让Nginx与我们的Python应用(服务名为app)的8000端口连接.除此之外, 我们还需要改下我们的.env配置文件, 把他们的host进行修改:

1
2
3
4
5
6
7
8
9
10
MYSQL_DB="mysql"
MYSQL_HOST="mysql"
MYSQL_PORT="3306"
MYSQL_USER="root"
MYSQL_PW=""

REDIS_URL="redis://redis"
REDIS_POOL_MINSIZE=1
REDIS_POOL_MAXSIZE=10
REDIS_ENCODING="utf-8"

还需要改下app的Dockerfile启动命令, 让他5秒后启动, 防止有些服务还没起来自己就先运行了:

1
CMD CMD sh -c 'sleep 5 && uvicorn --host 0.0.0.0 example:app'

万事俱备, 开始执行docker-compose up -d, 开始启动我们的容器群, 其中d是后台运行的意思. 然后自通过命令查看运行情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# 使用docker-compose up命令来查看应用的状态。输出中会显示容器名称、其中运行的Command、当前状态以及其监听的网络端口。
➜ version_3 git:(master) docker-compose ps
Name Command State Ports
-----------------------------------------------------------------------------------------------
version_3_app_1 uvicorn --host 0.0.0.0 exa ... Up 0.0.0.0:8000->8000/tcp, 8080/tcp
version_3_mysql_1 docker-entrypoint.sh mysql ... Up 0.0.0.0:33060->3306/tcp, 33060/tcp
version_3_nginx_1 /docker-entrypoint.sh ngin ... Up 0.0.0.0:8001->80/tcp
version_3_redis_1 docker-entrypoint.sh redis ... Up 0.0.0.0:63790->6379/tcp


# 使用docker-compose top命令列出各个服务(容器)内运行的进程。
# 其中PID编号是在Docker主机上(而不是容器内)的进程ID。
➜ version_3 git:(master) docker-compose top
version_3_app_1
UID PID PPID C STIME TTY TIME CMD
-------------------------------------------------------------------------------------------------------------------------
root 1802 1786 0 16:05 ? 00:00:00 /usr/local/bin/python /usr/local/bin/uvicorn --host 0.0.0.0 example:app

version_3_mysql_1
UID PID PPID C STIME TTY TIME CMD
---------------------------------------------------------------------------------------------------------------------------------
deepin-+ 1047 1018 0 16:05 ? 00:00:00 mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci

version_3_nginx_1
UID PID PPID C STIME TTY TIME CMD
------------------------------------------------------------------------------------------------
root 1355 1339 0 16:05 ? 00:00:00 nginx: master process nginx -g daemon off;
systemd+ 1467 1355 0 16:05 ? 00:00:00 nginx: worker process
systemd+ 1468 1355 0 16:05 ? 00:00:00 nginx: worker process
systemd+ 1469 1355 0 16:05 ? 00:00:00 nginx: worker process
systemd+ 1470 1355 0 16:05 ? 00:00:00 nginx: worker process
systemd+ 1471 1355 0 16:05 ? 00:00:00 nginx: worker process
systemd+ 1472 1355 0 16:05 ? 00:00:00 nginx: worker process
systemd+ 1473 1355 0 16:05 ? 00:00:00 nginx: worker process
systemd+ 1474 1355 0 16:05 ? 00:00:00 nginx: worker process
systemd+ 1475 1355 0 16:05 ? 00:00:00 nginx: worker process
systemd+ 1476 1355 0 16:05 ? 00:00:00 nginx: worker process
systemd+ 1477 1355 0 16:05 ? 00:00:00 nginx: worker process
systemd+ 1478 1355 0 16:05 ? 00:00:00 nginx: worker process

version_3_redis_1
UID PID PPID C STIME TTY TIME CMD
------------------------------------------------------------------
deepin-+ 1048 1014 0 16:05 ? 00:00:00 redis-server

# 查看目前的网络
# 查看network的详细信息 docker network inspect version_3_local-net
➜ version_3 git:(master) docker network ls
NETWORK ID NAME DRIVER SCOPE
b39273f15fb3 bridge bridge local
23ef7eb0fba0 host host local
ab8439cd985c none null local
5bcd17ecd747 version_3_local-net bridge local

# 查看卷
# 查看详情 docker volume inspect version_3_local-vol
➜ version_3 git:(master) docker volume ls
DRIVER VOLUME NAME
local version_3_local-vol

如果要停止, 可以使用docker-compose stop命令, 它停止应用,但并不会删除资源;对于已停止的Compose应用,可以使用docker-compose rm命令来删除。这会删除应用相关的容器和网络,但是不会删除卷和镜像;使用docker-compose down这一个命令就可以停止和关闭应用。应用被删除,仅留下了镜像、卷和源码。

docker-compose非常简单, 就是一个docker-composeyaml文件 以及几个命令, 但是对比前文提到的ansible还缺少了远程控制应用编排的功能, 一般情况下, docker-compose也只用于自己的电脑或者非生产机.

4.docker swarm

不够机器演示 Orz

查看评论