现在云原生越来越流行, 容器化势在必行, 所以也要学会如何把自己编写的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 ➜ 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
, 里面包含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, 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 (): 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 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 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写法都会导致镜像层数量的不同,每一个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
文件编写我们要忽略的文件即可:
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
通过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 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
在上面说过,在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 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 .
查看构建完的镜像, 镜像的大小已经减少很多了, 差不多只剩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 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
镜像创建完了, 开始启动容器:
1 2 ➜ 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 ➜ 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应用的镜像, 然后连接了本地的Mysql
和Redis
服务, 我们也可以把Mysql
和Redis
容器化, 不过这时候要一个一个Dockerfile来配置并使用的话就太麻烦了.如果有一个像Dockerfile
的文件, 文件写了如何安装这3个镜像, 然后我们执行一个命令就可以把这套环境安装到服务器上, 那就很方便了.在Docker
中Docker 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 -alpineRUN 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: "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 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
命令, 它停止应用,但并不会删除资源;对于已停止的Compose应用,可以使用docker-compose rm命令来删除。这会删除应用相关的容器和网络,但是不会删除卷和镜像;使用docker-compose down这一个命令就可以停止和关闭应用。应用被删除,仅留下了镜像、卷和源码。
docker-compose
非常简单, 就是一个docker-compose
yaml文件 以及几个命令, 但是对比前文提到的ansible
还缺少了远程控制应用编排的功能, 一般情况下, docker-compose
也只用于自己的电脑或者非生产机.
4.docker swarm 不够机器演示 Orz