现在云原生越来越流行, 容器化势在必行, 作为一个开发人员, 多多少少也要接触一些容器相关的操作, 其中最基础的操作是如何把自己的应用构建为一个Docker
容器, 并管理。 本文以starlette
框架和后台的开发常见3件套Nginx
, MySQL
和Redis
为底创建一个简单的Web后台演示项目, 并一步一步介绍如何编写成一个容器以及容器的运行。
注: 2021-02-07增加了示例代码 注: 2021-12-12部分内容进行重写
1.创建项目 第一步会先创建一个Python
后台项目, 这个项目包含3个接口, 一个接口用来测试服务是否正常, 另外一个是测试MySQL
调用, 最后一个是测试Redis
调用。
首先是创建一个后台项目以及依赖:
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 ➜ 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 (venv) ➜ python_on_docker git:(master) pip install starlette aiomysql aioredis uvicorn (venv) ➜ python_on_docker git:(master) pip install cryptography (venv) ➜ python_on_docker git:(master) python -m pip freeze > requirements.txt
之后创建项目主文件example.py
, 该文件主要提供API服务,里面包含上面所说的3个接口, 示例代码如下(源码 ):
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 78 79 from typing import Optional, Tupleimport aiomysqlimport aioredisfrom starlette.applications import Starlettefrom starlette.config import Configfrom starlette.requests import Requestfrom starlette.responses import JSONResponse, PlainTextResponsefrom starlette.routing import Route config: Config = Config(".env" ) mysql_pool: Optional[aiomysql.Pool] = None redis: Optional[aioredis.Redis] = None async def on_start_up (): """连接MySQL和Redis""" 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 (): """关闭MySQL和Redis连接""" 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: """测试MySQL调用接口""" 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: """测试Redis调用接口""" 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
(starlette
的config会自动加载.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 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 18 ➜ 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
中创建镜像很简单, 只需要通过一个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 FROM python:3.7 .4 -alpineLABEL maintainer="so1nxxxx@gmail.com" WORKDIR /data/app COPY . . ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONUNBUFFERED 1 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: 启动时运行的命令
这些命令都很简单, 不过我在COPY
和RUN
命令注释中, 都说到了新建了一个镜像层, 在Docker
镜像中, 每多一个镜像层就意味这这个镜像会占用更多存储空间, 同时也会更难用, 更慢。 所以大家一般都会追求构建的镜像都尽量的小, 不喜欢因为几个命令导致镜像占用过多的空间。 那我们该怎么区分命令, 判断哪些命令会新增一个镜像层呢?
关于如何区分命令是否会新建镜像层的一个基本原则是,如果指令的作用是向镜像中增添新的文件或者程序,那么这条指令就会新建镜像层, 如果只是告诉Docker
如何完成构建或者如何运行应用程序,那么就只会增加镜像的元数据。
此外, 不同的Dockerfile
写法都会导致镜像层数量的不同, 比如在Dockerfile
中每一个RUN
指令基本都会新增一个镜像层, 我们可以通过使用&&连接多个命令或者使用反斜杠(\)换行的方法,将多个命令包含在一个RUN指令中, 这样就可以减少镜像层的产生。
不过有些时候要把RUN
拆分的, 因为Docker
自带了一个缓存机制, 如果这个RUN
执行时构建的镜像层在缓存中时, Docker
会直接引用, 这样构建速度就会比较快, 而在把所有RUN
合并到同一条时, 基本上就很难命中缓存了(需要注意的是, docker在执行到第一句不命中缓存的命令后, 后面的所有命令是都不会通过缓存构建的)。
现在Dockerfile
编写完了, 在构建镜像之前, 我们先检查我们的目录:
1 2 (venv) ➜ python_on_docker git:(master) ls -a Dockerfile example.py requirements.txt __pycache__ venv .env
发现目录下面有__pycache__
和venv
文件, 这两个文件我是不想编进镜像里的, 但是开发的时候需要用到, 这时就可以使用Docker
中一个类似于gitignore
的机制, 我们通过.dockerignore
文件编写我们要忽略的文件即可让Docker
在构建镜像时忽略这些文件:
现在,Dockerfile
和.dockerignore
文件创建完了, 可以通过以下的命令开始构建自己的镜像了:
1 2 ➜ 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
可以看到我们的镜像已经创建成功了, 不过显示这个简单的镜像占用了435MB的空间, 这是不合理的, 我们可以通过docker image inspect xxx
查看镜像有多少层, 有哪些卷和配置信息, 进一步解决镜像过大的问题(由于返回的数据会比较多, 这里就不展示了)。此外, 我们还可以通过history
命令查看我们的镜像构建情况, 了解是哪一个步骤导致镜像变大的:
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 f7fedcb216b0 About a minute ago /bin/sh -c 190fd056b947 About a minute ago /bin/sh -c apk add --update gcc musl-dev pyt… 313MB 66901ff8b9d4 5 minutes ago /bin/sh -c 7e85b2fa504e 5 minutes ago /bin/sh -c a2714bff8c12 5 minutes ago /bin/sh -c dc4d69bd98e5 5 minutes ago /bin/sh -c db1533598434 5 minutes ago /bin/sh -c f309434dea3a 16 months ago /bin/sh -c <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
可以看到, 第3条命令的apk ...
那里占用的空间最大, 这是因为在RUN
的时候需要安装一些编译依赖后才能安装我们的想要的Python
库, 但这些依赖都非常大。 幸好Docker
针对这种情况提供了多阶段构建的功能(还有建造者模式, 但不如多阶段构建), 多阶段构建方式是一个Dockerfile
文件包含了多个FROM指令, 在这个文件中每一个FROM指令都是一个新的构建阶段(Build Stage),并且每个新的构建接单都可以方便地复制之前阶段完成的构件, 也就是可以先通过依赖构建一个比较重的Docker镜像, 然后基于该镜像构建一个用户真正想要的镜像, 最终只保留最后构建的镜像。
于是我们可以把我们的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 44 45 46 47 48 49 FROM python:3.7 .4 -alpine as builderWORKDIR /data/app COPY . . ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONUNBUFFERED 1 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 FROM python:3.7 .4 -alpineLABEL 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 ➜ docker image build -t app_1:latest .
构建完成后查看构建完的镜像:
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 ➜ 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 d4d38b71a1ba 43 seconds ago /bin/sh -c 5fb10c8afea8 43 seconds ago /bin/sh -c pip install --no-cache /data/pyth… 15.3MB e454bbe54adb 46 seconds ago /bin/sh -c d70a8a552490 46 seconds ago /bin/sh -c dc4d69bd98e5 14 minutes ago /bin/sh -c db1533598434 14 minutes ago /bin/sh -c f309434dea3a 16 months ago /bin/sh -c <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
可以看到现在的镜像大小已经减少很多了, 差不多只剩4分之一,非常完美! 如果还想让镜像更小一点, 那可以在使用bliud
命令时, 添加--squash
选项, 这样Docker
在build
的时候就会把所有镜像层合并为一个, 但这也是有缺点的, 因为合并的镜像层无法共享镜像层, 而且镜像在push
和pull
的时候开销会变得很大。
镜像终于创建完了, 终于可以启动容器查看我们构建的镜像的运行效果了:
1 2 ➜ docker container run -d --name docker_app_1 -p 8000:8000 app_1
调用了启动命令后, 通过ps
命令你给查看容器运行情况:
1 2 3 4 5 ➜ 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
通过输出可以发现镜像运行失败, 但是不知道为啥失败, 只能通过运行日志查看为什么失败:
1 2 3 4 5 6 7 8 9 10 11 12 # 查看运行日志 ➜ version_2 git:(master) docker logs -f -t --tail 10 docker_app_1 2021-02 -07 T09:01:16.062239955Z await pool._fill_free_pool(False) 2021-02 -07 T09:01:16.062241993Z File "/usr/local/lib/python3.7/site-packages/aiomysql/pool.py", line 168, in _fill_free_pool 2021-02 -07 T09:01:16.062250734Z **self._conn_kwargs) 2021-02 -07 T09:01:16.062253106Z File "/usr/local/lib/python3.7/site-packages/aiomysql/connection.py", line 75, in _connect 2021-02 -07 T09:01:16.062255305Z await conn._connect() 2021-02 -07 T09:01:16.062257318Z File "/usr/local/lib/python3.7/site-packages/aiomysql/connection.py", line 523, in _connect 2021-02 -07 T09:01:16.062259455Z self._host) from e 2021-02 -07 T09:01:16.062275244Z pymysql.err.OperationalError: (2003, "Can't connect to MySQL server on '127.0.0.1'") 2021-02 -07 T09:01:16.062277250Z 2021-02 -07 T09:01:16.062279127Z ERROR: Application startup failed. Exiting.
从报错日志可以看到, 镜像启动失败是由于连不上127.0.0.1
地址导致报错了, 可是本机上面已经安装了MySQL
啊, 为什么还会连不上呢? 这是因为Docker
容器运行的时候会选择一个网络模式, 共有host
、bridge
和none
三种网络可供配置:
bridge 该模式是Docker
的默认选项. bridge即桥接网络,以桥接模式连接到宿主机, 这时候容器内的应用访问127.0.0.1
是指容器本身的网络(在本机通过ifconfig
命令可以看到有个类似于docker0
的展示), 该模式下如果要连接到宿主机的网络, 只能把127.0.0.1
改为本机的局域网ip;
host host是宿主网络,即Docker
与宿主机共用网络, 在该模式下, Docker
容器内的应用使用网络时跟平时一样正常使用即可, 同时该模式的网络性能也是最好的, 如果在使用bridge模式时发现有网络瓶颈, 或者应用对网络延迟和并发有极高的要求时, 记得切为host网络模式。
none则表示无网络,容器内应用将无法联网.
了解了Docker
的网络模式后, 我们可以通过把网络模式改为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.单引擎模式部署和管理多容器应用 在懂了如何制作容器之后, 就可以开始准备成为Yaml工程师了, 上面我们只构建一个Python
应用的镜像, 然后连接了本地的MySQL
和Redis
服务, 现在我们也可以把MySQL
和Redis
这两个服务一起容器化, 不过这时候每个服务都通过一个特定Dockerfile
来配置并使用的话就太麻烦了。
如果有一个像Dockerfile
的文件, 文件里写了如何安装这3个镜像, 然后我们执行一个命令就可以把这三个服务都安装到服务器上, 那就很方便了, 在Docker
中是由Docker Compose
提供了这个功能。
如果用过Ansible
, 就知道会有一个palybook
的yaml配置文件, 只要在控制主机上存放一个palybook
文件, 就可以控制其它机器执行任何操作, 比如为其它主机安装应用等到, 而Docker Compose
有点类似, 基于该功能可以做到在一台机器上同时管理多个Docker
容器。
Docker Compose
通过一个声明式的配置文件描述整个应用,从而使用一条命令完成部署。应用部署成功后,还可以通过一系列简单的命令实现对其完整生命周期的管理。此外,配置文件还可以置于版本控制系统中进行存储和管理, 这个工具会跟docker
一起安装。
接下来就是实战了, 本次实例相比于之前的服务多了个Nginx
服务, 而且Nginx
是需要一个配置文件都,所以需要先为Nginx
单独配置一个Dockerfile
文件。
首先, 在目录创建一个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
,我们先不管, 继续创建Nginx
的Dockerfile
(源码 )文件:
1 2 3 4 5 6 FROM nginx:1.19 .0 -alpineRUN rm /etc/nginx/conf.d/default.conf COPY nginx.conf /etc/nginx/conf.d
现在, Nginx
的容器文件已经准备完毕, 开始编写我们的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 version: "3.5" services: redis: image: "redis:alpine" networks: - local-net ports: - target: 6379 published: 63790 mysql: image: mysql command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci restart: always networks: local-net: environment: MYSQL_DATABASE: 'test' MYSQL_USER: 'root' MYSQL_PASSWORD: '' MYSQL_ROOT_PASSWORD: '' MYSQL_ALLOW_EMPTY_PASSWORD: 'true' ports: - target: 3306 published: 33060 volumes: - type: volume source: local-vol target: /example_volumes app: build: . ports: - target: 8000 published: 8000 networks: - local-net volumes: - type: volume source: local-vol target: /example_volumes depends_on: - mysql - redis nginx: build: ./nginx networks: - local-net ports: - target: 80 published: 8001 depends_on: - app 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 11 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 ➜ 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 ➜ 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 ➜ 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 ➜ version_3 git:(master) docker volume ls DRIVER VOLUME NAMElocal version_3_local-vol
通过上述的几个命令可以发现服务是正常运行的, 如果这时候要停止, 可以使用docker-compose stop
命令, 它会停止应用,但并不会删除资源, 不过对于已停止的应用,可以使用docker-compose rm
命令来进行删除, 这会删除应用相关的容器和网络,但是不会删除卷和镜像。 而直接使用docker-compose down
这一个命令就可以停止和关闭应用, 然后应用被删除,仅留下了镜像、卷和源码。
4.总结 至此, Python应用的容器化就已经介绍完毕, 但是这只是一个简单的开始, 后面需要慢慢的了解多机的容器应用的怎么控制和执行的。