首页
docker run 如何让容器启动后不会自动停止

在刚开始接触 docker 的时候,发现通过docker run命令运行一个容器后,有时会莫名其妙地停止了。在网上搜索的答案都乱七八糟的,于是自己静下心来深入了解了一下。

首先,我们说一下直接运行 docker run --name [container_name] [image]:[tag] 这样的命令后会出现哪些情况:

  1. 容器启动后就结束了。 我们是没有任何感知的,通过 docker ps -a 可以看到容器的状态为 Exited (0)
  2. 容器正常启动并在终端运行。 重新开启一个终端,使用 docker ps 命令可以看到容器正常运行

这时候我们就纳闷了,为什么有些镜像我们运行后就自动退出了,有些会持续运行。首先给出答案,这完全取决于容器启动后执行的程序,也就是 Dockerfile 中指定的 CMDENTRYPOINT 执行的命令。

容器启动后默认执行命令的三种差异

CMDENTRYPOINT 都可指定容器启动后默认执行的命令。在制作 Docker 镜像时,Dockerfile 必须指定 CMDENTRYPOINT 其中的一个。所以我们去查看镜像里 CMDENTRYPOINT 执行了什么,就不难分析出原因。

通过 docker inspect [image_name] 命令可以查看镜像的详细信息,里面会包含 CMDENTRYPOINT。既然说容器启动后默认执行的命令有三种差异,那么我们就使用三个比较有代表性的镜像来演示一下吧。

image_1elnqf4tgmmaopv1u0b1u3k1432a.png-17.3kB

1. 启动一个持续运行的程序

如果一个镜像的默认执行命令是启动一个持续运行的程序,docker run 执行后它会正常启动并在终端运行。

我们拿 postgres 镜像来举个例子,先使用 docker inspect 命令查看镜像的详细信息。

$ docker inspect postgres:9.6-alpine

...

"Cmd": [
    "postgres"
],

...

可以看到容器在启动后会默认执行 postgres 程序,这个程序会持续运行。

我们使用 docker run命令启动一个 postgres 容器:

$ docker run --name postgres -e POSTGRES_PASSWORD=123456 postgres:9.6-alpine 

可以看到终端输出了一些信息并持续运行 postgres。开启另一个终端,运行 docker ps 可以看到容器的状态是 ok 的。

image_1elnritgv8kubmu1o791bbgp0034.png-118.7kB

2. 启动一个交互式 shell

有些镜像的默认执行命令是启动一个交互式 shell,直接运行 docker run 容器启动后就会结束。

我们拿 node 镜像来举个例子,同样先看一下镜像的详细信息。

$ docker inspect node:12.18.3-slim 

...

"Cmd": [
    "node"
],

...

可以看到容器在启动后会默认执行 node 程序,该程序会进入 node 的交互式 shell

我们使用 docker run 命令启动一个 node 容器:

$ docker run --name node node:12.18.3-slim

结果如前面所说,执行完后没有任何反应。通过 docker ps -a 可以看到容器的状态为 Exited (0)

那么对于这种情况,我们怎么让容器启动后不会停止呢?答案是使用-i-t-it 参数。

$ docker run --name node -it node:12.18.3-slim

可以看到,我们在终端进入了容器内部 node 的交互式 shell,保持容器一直运行。

image_1elntfbkt1p1m8nm123s1najdt43h.png-15.3kB

3. 直接运行一条普通的命令

如果某个镜像默认执行命令是一条普通的命令,例如:ls -l ,启动容器后必然会停止的。

我们拿 hello-world 镜像来举个例子。

$ docker inspect hello-world:latest

...

"Cmd": [
    "/hello"
],

...

同样使用 docker run 命令启动容器:

$ docker run --name hello-world hello-world:latest
$ docker run --name hello-world -it hello-world:latest

可以看到,即使加了 -it 参数也不会持续运行的。因为该镜像的目的就是输出一段内容就结束了,所以不要乱用 -it 参数。

image_1elnuld2ctdl173m1pc31oe214nd3u.png-73.9kB

如何在后台持续运行容器

通过前面的分析,我们知道了为什么容器启动后会停止。现在还有一个问题,就是我们希望容器启动后能够在后台去运行,不要占用终端,那么怎么做呢。

这个也很简单,只需要在 docker run 命令中加一个 -d 参数就可以了。-d 参数的作用是在后台运行容器并打印容器 ID。

--detach , -d		Run container in background and print container ID
  • 对于第一种情况,指定 -d 参数即可
$ docker run --name postgres -d -e POSTGRES_PASSWORD=123456 postgres:9.6-alpine 
  • 对于第二种情况,指定 -i-t 的同时,再指定一个 -d 参数,即 -dt-di
$ docker run --name node -dt node:12.18.3-slim

至于 -i-t 的区别,在后面的文章会介绍到。

指导 Dockerfile 的 编写

在实际的使用中,很多人不管什么镜像,都使用 docker run -dt,虽然也不知道是什么意思,但是容器最终都会正常运行起来的。因为大家拉取的镜像,除了 hello-world 这种做演示用的,基本所有的都是前两种情况。所以在这方面基本不会踩坑。

大家踩坑比较多的就是自己使用 Dockerfile 来构建一个镜像,运行后经常就跑不起来。主要是对 CMDENTRYPOINT 理解得不透彻,不知道用哪个指令以及指令里面具体要写什么。

那么我们就结合前面所说的,来讲一下如何编写一个可以让容器启动后持续运行的 Dockerfile

基础镜像默认执行命令是启动一个持续运行的程序

nginxmysqlpostgres 这些镜像默认执行命令是启动一个持续运行的程序。我们用它作为一个基础镜像,不需要指定 CMDENTRYPOINT,它都能正常运行。

需要注意的是,如果在 Dockerfile 中指定了CMDENTRYPOINT,它会覆盖掉基础镜像的默认执行命令。比如下面这个 Dockerfile

FROM nginx:latest
CMD ["nginx"]

我们用它构建一个镜像并启动容器:

$ docker build -t nginx:test .
$ docker run nginx:test

发现它启动后就退出了。

基础镜像默认执行命令是启动一个交互式 shell

除了上述情况,CMD 应使用交互式 shell,例如:node。如果我们在 Dockerfile 中不指定 CMDENTRYPOINT,它会使用基础镜像的默认行为。

同样,我们自己指定了CMDENTRYPOINT,也需要注意是否启动了一个持续运行的程序或者启动了一个交互式 shell。

例如下面这个 Dockerfile,在 ENTRYPOINT 中使用了一条普通的命令:

FROM node:12.18.3-slim
ENTRYPOINT node -v

我们用它构建一个镜像并启动容器:

$ docker build -t node:test .
$ docker run node:test 

容器启动后打印 node 的版本号后就退出了。

我们也可以利用交互式 shell 来启动一个持续运行的程序。比如用 node 来启动一个服务。我们在 Dockerfile 的同级目录中新建一个 server.js,里面的内容是nodejs 的一个最简单的例子,启动一个 http 服务。

image_1elpeukn41fug1hom2l7t48hat9.png-20.5kB

server.js

var http = require('http');

http.createServer(function (request, response) {

    // 发送 HTTP 头部 
    // HTTP 状态值: 200 : OK
    // 内容类型: text/plain
    response.writeHead(200, {'Content-Type': 'text/plain'});

    // 发送响应数据 "Hello World"
    response.end('Hello World\n');
}).listen(8888);

// 终端打印如下信息
console.log('Server running at http://127.0.0.1:8888/');

Dockerfile

FROM node:12.18.3-slim
RUN mkdir /app
COPY . /app
WORKDIR /app
ENTRYPOINT node server.js

我们构建镜像后直接用 docker run 就可以在终端持续运行该容器。

总结

根据CMDENTRYPOINT 指定的程序不同,有三种情况需要考虑:

1. 对于一个持续运行的程序,直接执行 docker run 容器就可以在终端持续运行。指定 -d 参数,就可以在后台持续运行

2. 对于一个交互式 shell,指定 -it 参数,容器就可以在终端持续运行。指定 -dt 参数,就可以在后台持续运行

3. 对于一个执行完就结束的命令,容器启动后执行完命令就会结束。

在自己编写 Dockerfile 的时候,要清楚自己写的 CMDENTRYPOINT 是干吗的,同样也要清楚基础镜像里的 CMDENTRYPOINT 指定的程序是什么。

延伸阅读

  • 如何正确使用 docker run -i、 -t、-d 参数

  • Dockerfile RUN、CMD 和 ENTRYPOINT 的区别

  • 使用 nginx -g daemon off 启动 nginx 容器的原因

  • Docker 使用 attach 与 exec 命令进入容器的区别