在 Docker Compose 中让容器保持运行

1、概览

一个 Docker 容器会运行一个进程、应用程序,有时仅是一个脚本或命令,以执行其设计任务。

每个容器一旦内部没有任何进程或脚本运行,就会停止并退出。有些容器默认会持续运行,直到用户选择停止它们,例如 MySQL 数据库容器、Spring Boot Web 应用容器或 SMTP 邮件服务器容器。但有时,我们需要让容器在其主要任务完成后仍然保持运行,比如 Ubuntu 容器。

本文将带你了解如何使用 Docker Compose 实现这一需求。

2、Docker Compose 设置

Docker Compose 是我们用来定义和运行多容器服务的工具。唯一的前提条件是在受支持的操作系统平台上安装 Docker(包括 Docker Server、Docker Client 和 Docker Compose)。

本教程中我们使用 Linux Ubuntu。

3、运行 Ubuntu 容器

我们使用一个名为 docker-compose.yml 的示例 Docker Compose 配置文件来定义服务:

services:
  demo:
    image: ubuntu

3.1、启动 Docker Compose 服务

使用 -d 选项在后台运行服务的容器:

$ docker-compose up -d

输出如下,显示它创建了一个容器:

Creating ubuntu_demo_1 ... done

查看 Docker Compose 服务创建的容器是否正在运行。

使用 docker ps -a 命令列出容器:

$ docker ps -a

结果显示了一个包含运行中和已退出容器的列表:

CONTAINER ID   IMAGE     COMMAND          CREATED          STATUS                     PORTS     NAMES
a66cec15e72c   ubuntu    "/bin/bash"      10 seconds ago   Exited (0) 9 seconds ago             ubuntu_demo_1

由 Docker Compose 服务启动的 ubuntu_demo_1 容器在创建后不久便会退出

设置完成后,来看看保持容器运行的几种方法:

  • 分配 TTY
  • 使用 tail 命令
  • 使用 sleep 命令
  • 使用死循环脚本

3.2、测试间的清理工作

在逐一尝试这些示例时,最好在每个演示的策略之间使用 docker-compose down 命令来清理容器。

$ docker-compose down
Stopping ubuntu_demo_1 ... done
Removing ubuntu_demo_1 ... done
Removing network ubuntu_default

该命令还会删除为 Docker Compose 服务创建的所有网络(Network)和卷(Volume)。

4、分配 TTY

可以为示例 Ubuntu 容器分配一个伪终端(pseudo-TTY)来保持其运行。之所以称为 “伪” 终端,是因为没有使用物理设备驱动程序;伪终端驱动程序使容器看起来像终端连接会话。

通过向示例配置文件中添加键值对 tty: true 来分配 TTY:

services:
  demo:
    image: ubuntu
    tty: true

现在,使用相同的命令创建服务容器:

$ docker-compose up -d

可以看到它仍在运行:

$ docker ps
CONTAINER ID   IMAGE     COMMAND       CREATED          STATUS          PORTS     NAMES
dcc44f90a9eb   ubuntu    "/bin/bash"   16 seconds ago   Up 13 seconds             ubuntu_demo_1

由 Docker Compose 服务启动的 ubuntu_demo_1 容器正在运行。当我们附加 TTY 时,Docker 守护进程会保持主进程运行,从而使容器持续运行。

5、使用 tail 命令

另一种保持容器运行的方法是使用 tail 命令:

services:
  demo:
    image: ubuntu
    command: "tail -f /dev/null"

我们在这里使用 tail 命令及其 -f 选项来持续从 TTY 上的 stdin 读取数据。通过指定 /dev/nulltail 会永久 “读取” 该设备,从而使容器保持运行状态。

启动 Docker Compose 服务,随后列出所有运行中的容器:

$ docker ps
CONTAINER ID   IMAGE     COMMAND               CREATED          STATUS          PORTS     NAMES
5b293dbf6cc9   ubuntu    "tail -f /dev/null"   18 seconds ago   Up 17 seconds             ubuntu_demo_1

可以看到,ubuntu_demo_1 容器仍在运行。

6、使用 sleep 命令

还可以通过 sleep 命令保持容器运行:

services:
  demo:
    image: ubuntu
    command: "sleep infinity"

这种方法之所以有效,是因为它在容器的 bash shell 中创建了一个 TTY。使用 infinity 参数意味着该命令永远不会完成,从而使容器永久保持运行状态。

再次创建服务容器,随后列出所有运行中的容器:

$ docker ps
CONTAINER ID   IMAGE     COMMAND               CREATED          STATUS          PORTS     NAMES
c55a8b1f3839   ubuntu    "sleep infinity"      2 minutes ago    Up 2 minutes              ubuntu_demo_1

同样,容器正在运行。

7、使用死循环脚本

此外,我们还可以在 Docker Compose 启动的容器中运行一个由 whiledo 命令组合的死循环脚本,使容器保持运行

如下:

services:
  demo:
    image: ubuntu
    command: "sh -c 'while true; do sleep 1; done'"

运行修改后的 Docker Compose 服务,随后列出所有运行中的容器:

$ docker ps
CONTAINER ID   IMAGE     COMMAND                     CREATED           STATUS          PORTS     NAMES
1f690849f774   ubuntu    "sh -c 'while true; …"      14 seconds ago    Up 12 seconds              ubuntu_demo_1

Docker 容器持续运行是因为 while true 条件始终为 true

与单纯使用 sleep infinity 命令相比,无限 while 循环脚本的优势在于:用户可以在循环内执行其他命令来完成诸如输出日志信息或检查进程状态等任务。

8、为什么这对 hello-world 无效?

注意,并非所有类型的容器都能通过这些方法保持运行。我们上述所有示例的前提是:能在容器内启动 bash shell 来执行 Linux 命令。这对于使用 ubuntu 镜像或其他基于 Linux 的镜像创建的 Docker Compose 服务有效。

但以 hello-world 镜像为基础的容器就无法保持运行。要理解其原因,来看下 hello-world 镜像的 Dockerfile:

FROM scratch
COPY hello /
CMD ["/hello"]

scratch 镜像通常用于构建基础镜像,其本身并不提供 Linux 环境。没有 Linux 环境,就无法在 shell 中运行命令。

9、总结

本文介绍了通过 Docker Compose 保持 Docker 容器运行的不同方法。需要注意的是,并非所有 Docker 容器都需要显式配置来保持运行。因为部分 Docker 容器会默认启动内部应用程序或进程作为常规功能,这些进程会自然维持容器运行。

此外,并非所有类型的容器都能被保持运行,因为有些容器本身不支持运行 Bash shell 或其他 Linux 命令。


Ref:https://www.baeldung.com/docker-compose-keep-container-running