docker-compose

现代应用程序由多个应用组成,比如 Web 应用里可能有 Mysql 用来当数据库、Redis 用来做数据缓存。

当开启一个应用容器时,需要先将 Mysql 和 Redis 启动起来,再将应用容器运行起来。

同时,我们还需要在有互相通信的容器之间创建网络,以便他们进行数据交换。

如果我们对容器进行了各种配置,我们还需要将容器创建和配置的命令保存下来,以便下次可以直接使用。

为了应对上述那么多复杂的场景,Docker 给出了 docker-compose 方案。

如果说 Dockerfile 能够对容器内运行的环境的搭建固化下来,那么 Docker Compose 就可以理解成将多个容器运行的方式和配置固化到一个 yml 文件中,再通过这个 yml 文件启动多个容器。

nest 项目 docker-compose 示例

Docker-compose 的步骤分成三步:

  • 编写容器所需的镜像的 Dockerfile。(非必须)
  • 编写用于配置容器的 docker-compose.yml 描述文件。
  • docker-compose 命令启动应用。

最核心的是第二部,下面是部署 nestjs app 的示例:

yaml
1services:
2  server:
3    build:
4      context: .
5    restart: on-failure:2
6    environment:
7      - NODE_ENV=production
8      - DATABASE_URL=postgresql://postgres:root@db:5432/postgres?schema=public
9    ports:
10      - 8888:3000
11    depends_on:
12      db:
13        condition: service_healthy
14
15  db:
16    image: postgres:14
17    restart: always
18    user: postgres
19    secrets:
20      - db-password
21    volumes:
22      - db-data:/var/lib/postgresql/data
23    environment:
24      - POSTGRES_DB=postgres
25      - POSTGRES_PASSWORD_FILE=/run/secrets/db-password
26    expose:
27      - 5432
28    ports:
29      - 5432:5432
30    healthcheck:
31      test: ['CMD', 'pg_isready']
32      interval: 10s
33      timeout: 5s
34      retries: 5
35
36  redis:
37    image: redis:7.0
38    restart: unless-stopped
39    volumes:
40      - redis-data:/data
41    expose:
42      - 6379
43    ports:
44      - 6379:6379
45
46volumes:
47  db-data:
48  redis-data:
49secrets:
50  db-password:
51    file: db/password.txt
  1. 定义服务

    Docker Compose 中一个服务对应一个容器,redis、server、db 就是服务的名称。对服务的定义跟之前的创建和启动容器非常相似,Docker Compose 会从配置文件中读取出这些内容,帮助我们创建和管理这些容器。

  2. 指定镜像

    容器的基础就是镜像,所以每个服务需要指定镜像。

    我们可以通过两种方式指定镜像,一种是通过 image 配置,这个非常简单,只需要能给出在 Docker Hub 中找到的镜像的名称即可。

    另外一种是直接采用 Dockerfile 来构建镜像,通过 build 这个配置我们能够定义构建的环境目录,相当于执行docker build。如果我们用这个方式指定镜像,那么 Docker-Compose 会先执行镜像的构建,再通过这个镜像启动容器。

    下面是一个常用的 build选项的配置

    yaml
    1webapp:
    2  build:
    3    context: ./webapp
    4    dockerfile: webapp-dockerfile
    5    args:
    6      - JAVA_VERSION=1.6

    context表示路径,dockerfile 表示 Dockerfile 的文件名,args表示构建的参数。

    command配置能够指定基于该镜像启动容器时执行的命令,相当于替代 Dockerfile 中的 CMD

    environment配置则是环境变量,例如例子中给 POSTGRES 设置了初始密码

  3. 依赖声明

    如果我们的服务间有非常强的依赖关系、先后顺序关系,就必须告知 Docker Compose 容器的启动顺序。只有当被依赖的容器完全启动后,Docker Compose 才能创建和启动这个容器。

    定义依赖关系是用depends_on这个配置项。

  4. 文件挂载和数据卷

    文件挂载和数据卷都用volumes字段指定,区别在于一个是使用文件的路径,一个是使用卷的名字。

    在开发时,推荐将代码挂载到容器里,这样在代码修改后,容器内也可以马上做出反应。

    Docker Compose 也能够自动完成对数据卷的创建,在上面的例子中,独立于 services 的 volumes 配置是用来声明数据卷的。使用数据卷则是在servicesvolumes里用<Volume Name>:<Container dataStorage>这种形式定义。

  5. 端口映射

    在 Docker Compose 的每个服务配置里,我们还看到了 ports 这个配置项,它是用来定义端口映射的。

    Docker CLI中相当于-p选项,用来指定宿主机和容器中的端口映射。

通过 docker compose up命令,即可创建包含多个服务的 docker-compose。

示例代码

重启策略

node 服务往往用 pm2 来守护进程,在服务器上跑 node 时,一般都会配置 pm2。

pm2 在进程崩溃时也有重启进程的功能。

docker 支持自动重启,所以我们用 docker 方案就不需要 pm2 了。

有 4 种重启策略:

  • no: 容器退出不自动重启(默认值)
  • always:容器退出总是自动重启,除非 docker stop。
  • on-failure:容器非正常退出才自动重启,还可以指定重启次数,如 on-failure:5
  • unless-stopped:容器退出总是自动重启,除非 docker stop

以上面的示例产生的 docker-compose 为例,

docker-compose 的 server 是这么配的

yml
1restart: on-failure:2

db 是这么配的

yml
1restart: always

redis 是这么配的

yml
1restart: unless-stopped

我们在项目源代码的 main.js 中特意写一个 error:

jade
1async function bootstrap() {
2  const app = await NestFactory.create(AppModule);
3  throw new Error('This is an error');
4  await app.listen(3000);
5}

然后docker compose up看一下生成的 compose 容器。

image-20240328201858071

点开状态为Exited的容器,会发现内部报错了,并且重启了一次。

image-20240328201441421

这就是on-failure的作用,当遇到非正常情况时,会重启并且可以设置重启次数。

alwaysunless-stopped有什么差别呢?

差别在于 Docker Deamon 退出的时候。

现在我们关闭这两个容器

image-20240328202620761

然后重启 docker。

当 docker 重启时,docker Engine 也会启动,docker Engine 是 Docker Deamon 的后台进程。

image-20240328202710795

当 Docker Deamon 启动完成后,原来策略为 always的容器会自动打开,而策略为unless-stopped的容器还是关闭状态。

image-20240328203038543

这就是两者之间的差别。

总结

docker compose 是记录容器的各种配置的 yaml 文件,通过它可以描述容器的配置、启动、挂载、网络、依赖关系等。

Docker 也是支持自动重启的,可以在 docker run 的时候通过 --restart 指定重启策略,或者 Docker Compose 配置文件里配置 restart。

有 4 种重启策略:

  • no: 容器退出不自动重启(默认值)
  • always:容器退出总是自动重启,除非 docker stop。
  • on-failure:容器非正常退出才自动重启,还可以指定重启次数,如 on-failure:5
  • unless-stopped:容器退出总是自动重启,除非 docker stop

always 和 unless-stopped 的差别在于 always策略的容器会在 Docker Deamon 启动时启动,而unless-stopped不会。

绝大多数情况下,直接用 Docker 跑 node 脚本就行,不需要 PM2。