docker compose 介绍

用于单机环境的容器编排,把一次性要创建的镜像、容器、网络、数据卷等内容写到docker-compose.yml里去,然后通过docker-compose up一条指令把全部的容器都跑起来。

除了可以批量启动容器,docker compose还可以实现水平扩展、管理环境变量、指定容器启动顺序、健康检查等功能。

docker compose 的安装

Windows和Mac在默认安装了docker desktop以后,docker-compose随之自动安装。

PS C:\Users\Peng Xiao\docker.tips> docker-compose --version
docker-compose version 1.29.2, build 5becea4c

Linux用户需要自行安装,最新版本号可以在这里查询 https://github.com/docker/compose/releases

$ sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose
$ docker-compose --version
docker-compose version 1.29.2, build 5becea4c

熟悉python的朋友,可以使用pip去安装docker-compose:

$ pip install docker-compose

compose文件语法结构

docker compose文件的语法说明 https://docs.docker.com/compose/compose-file/

以下是一个典型的compose文件示例:

version: "3.8"

services: # 容器
  servicename: # 服务名字,这个名字也是内部 bridge网络可以使用的 DNS name
    image: # 镜像的名字
    command: # 可选,如果设置,则会覆盖默认镜像里的 CMD命令
    environment: # 可选,相当于 docker run里的 --env
    volumes: # 可选,相当于docker run里的 -v
    networks: # 可选,相当于 docker run里的 --network
    ports: # 可选,相当于 docker run里的 -p
  servicename2:

volumes: # 可选,相当于 docker volume create

networks: # 可选,相当于 docker network create


Python Flask + Redis 练习为例子,改造成一个docker-compose文件:

shell脚本
docker image pull redis
docker image build -t flask-demo .

# create network
docker network create -d bridge demo-network

# create container
docker container run -d --name redis-server --network demo-network redis
docker container run -d --network demo-network --name flask-demo --env REDIS_HOST=redis-server -p 5000:5000 flask-demo
docker-compose.yml
version: "3.8"

services:
  flask-demo:
    image: flask-demo:latest
    environment:
      - REDIS_HOST=redis-server
    networks:
      - demo-network
    ports:
      - 8080:5000

  redis-server:
    image: redis:latest
    networks:
     - demo-network

networks:
  demo-network:

docker compose 水平扩展

源码结构

├── docker-compose.yml
└── flask
    ├── Dockerfile
    └── app.py
app.py
from flask import Flask
from redis import Redis
import os
import socket

app = Flask(__name__)
redis = Redis(host=os.environ.get('REDIS_HOST', '127.0.0.1'), port=6379)


@app.route('/')
def hello():
    redis.incr('hits')
    return f"Hello Container World! I have been seen {redis.get('hits').decode('utf-8')} times and my hostname is {socket.gethostname()}.\n"
Dockerfile
FROM python:3.9.5-slim

RUN pip install flask redis && \
    groupadd -r flask && useradd -r -g flask flask && \
    mkdir /src && \
    chown -R flask:flask /src

USER flask

COPY app.py /src/app.py

WORKDIR /src

ENV FLASK=app.py REDIS_HOST=redis

EXPOSE 5000

CMD ["flask", "run", "-h", "0.0.0.0"]
docker-compose.yml
version: "3.8"

services:
  flask:
    build:
      context: ./flask
      dockerfile: Dockerfile
    image: flask-demo:latest
    environment:
      - REDIS_HOST=redis-server

  redis-server:
    image: redis:latest

  client:
    image: xiaopeng163/net-box:latest
    command: sh -c "while true; do sleep 3600; done;"

环境清理

删除容器和之前构建的镜像:

$ docker container rm -f $(docker container ps -aq)
$ docker image rm flask

启动

$ docker-compose pull
$ docker-compose build
$ docker-compose up -d
Creating network "compose-scale-example_default" with the default driver
Creating compose-scale-example_flask_1        ... done
Creating compose-scale-example_client_1       ... done
Creating compose-scale-example_redis-server_1 ... done
$ docker-compose ps
                Name                              Command               State    Ports
----------------------------------------------------------------------------------------
compose-scale-example_client_1         sh -c while true; do sleep ...   Up
compose-scale-example_flask_1          flask run -h 0.0.0.0             Up      5000/tcp
compose-scale-example_redis-server_1   docker-entrypoint.sh redis ...   Up      6379/tcp

水平扩展 scale

$ docker-compose up -d --scale flask=3
compose-scale-example_client_1 is up-to-date
compose-scale-example_redis-server_1 is up-to-date
Creating compose-scale-example_flask_2 ... done
Creating compose-scale-example_flask_3 ... done
$ docker-compose ps
                Name                              Command               State    Ports
----------------------------------------------------------------------------------------
compose-scale-example_client_1         sh -c while true; do sleep ...   Up
compose-scale-example_flask_1          flask run -h 0.0.0.0             Up      5000/tcp
compose-scale-example_flask_2          flask run -h 0.0.0.0             Up      5000/tcp
compose-scale-example_flask_3          flask run -h 0.0.0.0             Up      5000/tcp
compose-scale-example_redis-server_1   docker-entrypoint.sh redis ...   Up      6379/tcp

添加nginx实现负载均衡

源码结构如下,增加nginx作为反向代理:

├── docker-compose.yml
├── flask
│   ├── Dockerfile
│   └── app.py
└── nginx
    └── nginx.conf
nginx.conf
server {
  listen  80 default_server;
  location / {
    proxy_pass http://flask:5000;
  }
}
docker-compose.yml
version: "3.8"

services:
  flask:
    build:
      context: ./flask
      dockerfile: Dockerfile
    image: flask-demo:latest
    environment:
      - REDIS_HOST=redis-server
    networks:
      - backend
      - frontend

  redis-server:
    image: redis:latest
    networks:
      - backend

  nginx:
    image: nginx:stable-alpine
    ports:
      - 8000:80
    depends_on:
      - flask
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./var/log/nginx:/var/log/nginx
    networks:
      - frontend

networks:
  backend:
  frontend:

验证步骤:

  1. 运行docker-compose up -d
  2. 浏览器访问localhost:8000
  3. 刷新网页
  4. 运行docker-compose up -d --scale flask=3,把flask容器水平扩展成3个
  5. 再次刷新网页,可以看到请求被平摊到3个flask容器上。

docker compose 环境变量

参考文档:https://docs.docker.com/compose/environment-variables/

通过.env文件指定docker-compose要用到的环境变量,通过environment字段设置容器运行的环境变量。

.env文件一般会添加到.gitignore和.dockerignore中。


下面的示例通过环境变量来设置redis镜像启动时的密码,flask通过读取系统环境变量获得redis主机的密码。

├── .env
├── docker-compose.yml
├── flask
│   ├── Dockerfile
│   └── app.py
└── nginx
    └── nginx.conf
app.py
from flask import Flask
from redis import StrictRedis
import os
import socket

app = Flask(__name__)
redis = StrictRedis(host=os.environ.get('REDIS_HOST', '127.0.0.1'),
                    port=6379, password=os.environ.get('REDIS_PASS'))


@app.route('/')
def hello():
    redis.incr('hits')
    return f"Hello Container World! I have been seen {redis.get('hits').decode('utf-8')} times and my hostname is {socket.gethostname()}.\n"
.env
REDIS_PASSWORD=ABC123
docker-compose.yml
version: "3.8"

services:
  flask:
    build:
      context: ./flask
      dockerfile: Dockerfile
    image: flask-demo:latest
    environment:
      - REDIS_HOST=redis-server
      - REDIS_PASS=${REDIS_PASSWORD}
    networks:
      - backend
      - frontend

  redis-server:
    image: redis:latest
    command: redis-server --requirepass ${REDIS_PASSWORD}
    networks:
      - backend

  nginx:
    image: nginx:stable-alpine
    ports:
      - 8000:80
    depends_on:
      - flask
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./var/log/nginx:/var/log/nginx
    networks:
      - frontend

networks:
  backend:
  frontend:

docker compose 服务依赖和健康检查

关于健康检查:

健康检查是容器运行状态的高级检查,主要是检查容器所运行的进程是否能正常的对外提供“服务”,比如一个数据库容器,我们不光 需要这个容器是up的状态,我们还要求这个容器的数据库进程能够正常对外提供服务,这就是所谓的健康检查。

容器的健康检查

容器本身有一个健康检查的功能,但是需要在Dockerfile里定义,或者在执行docker container run 的时候,通过下面的一些参数指定:

--health-cmd string              Command to run to check health
--health-interval duration       Time between running the check
                                (ms|s|m|h) (default 0s)
--health-retries int             Consecutive failures needed to
                                report unhealthy
--health-start-period duration   Start period for the container to
                                initialize before starting
                                health-retries countdown
                                (ms|s|m|h) (default 0s)
--health-timeout duration        Maximum time to allow one check to

示例源码

我们以下面的这个flask容器为例,相关的代码如下:

app.py
 from flask import Flask
from redis import StrictRedis
import os
import socket

app = Flask(__name__)
redis = StrictRedis(host=os.environ.get('REDIS_HOST', '127.0.0.1'),
                    port=6379, password=os.environ.get('REDIS_PASS'))


@app.route('/')
def hello():
    redis.incr('hits')
    return f"Hello Container World! I have been seen {redis.get('hits').decode('utf-8')} times and my hostname is {socket.gethostname()}.\n"
 
Dockerfile
FROM python:3.9.5-slim

RUN pip install flask redis && \
    apt-get update && \
    apt-get install -y curl && \
    groupadd -r flask && useradd -r -g flask flask && \
    mkdir /src && \
    chown -R flask:flask /src

USER flask

COPY app.py /src/app.py

WORKDIR /src

ENV FLASK_APP=app.py REDIS_HOST=redis

EXPOSE 5000

HEALTHCHECK --interval=30s --timeout=3s \
    CMD curl -f http://localhost:5000/ || exit 1

CMD ["flask", "run", "-h", "0.0.0.0"]
上面Dockerfili里的HEALTHCHECK 就是定义了一个健康检查。 会每隔30秒检查一次,如果失败就会退出,退出代码是1。

构建镜像和创建容器

构建镜像,创建一个bridge网络,然后启动容器连到bridge网络:

$ docker image build -t flask-demo .
$ docker network create mybridge
$ docker container run -d --network mybridge --env REDIS_PASS=abc123 flask-demo

查看容器状态:

$ docker container ls
CONTAINER ID   IMAGE        COMMAND                  CREATED       STATUS                            PORTS      NAMES
059c12486019   flask-demo   "flask run -h 0.0.0.0"   4 hours ago   Up 8 seconds (health: starting)   5000/tcp   dazzling_tereshkova

也可以通过docker container inspect 059 查看详情, 其中有有关health的:

"Health": {
"Status": "starting",
"FailingStreak": 1,
"Log": [
    {
        "Start": "2021-07-14T19:04:46.4054004Z",
        "End": "2021-07-14T19:04:49.4055393Z",
        "ExitCode": -1,
        "Output": "Health check exceeded timeout (3s)"
    }
]
}

经过3次检查,一直是不通的,然后health的状态会从starting变为 unhealthy:

docker container ls
CONTAINER ID   IMAGE        COMMAND                  CREATED       STATUS                     PORTS      NAMES
059c12486019   flask-demo   "flask run -h 0.0.0.0"   4 hours ago   Up 2 minutes (unhealthy)   5000/tcp   dazzling_tereshkova

启动redis服务器

启动redis,连到mybridge上,name=redis, 注意密码:

$ docker container run -d --network mybridge --name redis redis:latest redis-server --requirepass abc123

经过几秒钟,我们的flask 变成了healthy:

$ docker container ls
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS                   PORTS      NAMES
bc4e826ee938   redis:latest   "docker-entrypoint.s…"   18 seconds ago   Up 16 seconds            6379/tcp   redis
059c12486019   flask-demo     "flask run -h 0.0.0.0"   4 hours ago      Up 6 minutes (healthy)   5000/tcp   dazzling_tereshkova


一个healthcheck不错的例子 https://gist.github.com/phuysmans/4f67a7fa1b0c6809a86f014694ac6c3a

服务依赖

// TODO

docker compose 投票 app 练习

源码地址: https://github.com/dockersamples/example-voting-app



  • 无标签