在刚开始接触 docker 的时候,发现通过docker run
命令运行一个容器后,有时会莫名其妙地停止了。在网上搜索的答案都乱七八糟的,于是自己静下心来深入了解了一下。
首先,我们说一下直接运行 docker run --name [container_name] [image]:[tag]
这样的命令后会出现哪些情况:
- 容器启动后就结束了。 我们是没有任何感知的,通过
docker ps -a
可以看到容器的状态为Exited (0)
- 容器正常启动并在终端运行。 重新开启一个终端,使用
docker ps
命令可以看到容器正常运行
这时候我们就纳闷了,为什么有些镜像我们运行后就自动退出了,有些会持续运行。首先给出答案,这完全取决于容器启动后执行的程序,也就是 Dockerfile
中指定的 CMD
或 ENTRYPOINT
执行的命令。
容器启动后默认执行命令的三种差异
CMD
或 ENTRYPOINT
都可指定容器启动后默认执行的命令。在制作 Docker 镜像时,Dockerfile
必须指定 CMD
或 ENTRYPOINT
其中的一个。所以我们去查看镜像里 CMD
或 ENTRYPOINT
执行了什么,就不难分析出原因。
通过 docker inspect [image_name]
命令可以查看镜像的详细信息,里面会包含 CMD
和 ENTRYPOINT
。既然说容器启动后默认执行的命令有三种差异,那么我们就使用三个比较有代表性的镜像来演示一下吧。
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 的。
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,保持容器一直运行。
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
参数。
如何在后台持续运行容器
通过前面的分析,我们知道了为什么容器启动后会停止。现在还有一个问题,就是我们希望容器启动后能够在后台去运行,不要占用终端,那么怎么做呢。
这个也很简单,只需要在 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
来构建一个镜像,运行后经常就跑不起来。主要是对 CMD
和 ENTRYPOINT
理解得不透彻,不知道用哪个指令以及指令里面具体要写什么。
那么我们就结合前面所说的,来讲一下如何编写一个可以让容器启动后持续运行的 Dockerfile
。
基础镜像默认执行命令是启动一个持续运行的程序
像 nginx
、mysql
、postgres
这些镜像默认执行命令是启动一个持续运行的程序。我们用它作为一个基础镜像,不需要指定 CMD
和 ENTRYPOINT
,它都能正常运行。
需要注意的是,如果在 Dockerfile
中指定了CMD
和 ENTRYPOINT
,它会覆盖掉基础镜像的默认执行命令。比如下面这个 Dockerfile
:
FROM nginx:latest
CMD ["nginx"]
我们用它构建一个镜像并启动容器:
$ docker build -t nginx:test .
$ docker run nginx:test
发现它启动后就退出了。
基础镜像默认执行命令是启动一个交互式 shell
除了上述情况,CMD
应使用交互式 shell,例如:node
。如果我们在 Dockerfile
中不指定 CMD
和 ENTRYPOINT
,它会使用基础镜像的默认行为。
同样,我们自己指定了CMD
和 ENTRYPOINT
,也需要注意是否启动了一个持续运行的程序或者启动了一个交互式 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 服务。
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
就可以在终端持续运行该容器。
总结
根据CMD
和 ENTRYPOINT
指定的程序不同,有三种情况需要考虑:
1. 对于一个持续运行的程序,直接执行 docker run
容器就可以在终端持续运行。指定 -d
参数,就可以在后台持续运行
2. 对于一个交互式 shell,指定 -it
参数,容器就可以在终端持续运行。指定 -dt
参数,就可以在后台持续运行
3. 对于一个执行完就结束的命令,容器启动后执行完命令就会结束。
在自己编写 Dockerfile
的时候,要清楚自己写的 CMD
和 ENTRYPOINT
是干吗的,同样也要清楚基础镜像里的 CMD
和 ENTRYPOINT
指定的程序是什么。
延伸阅读
-
如何正确使用 docker run -i、 -t、-d 参数
-
Dockerfile RUN、CMD 和 ENTRYPOINT 的区别
-
使用 nginx -g daemon off 启动 nginx 容器的原因
-
Docker 使用 attach 与 exec 命令进入容器的区别