优雅地停止 Docker 容器

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...点击查看项目介绍 ;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;

截止目前, 星球 内专栏累计输出 54w+ 字,讲解图 2476+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 1900+ 小伙伴加入学习 ,欢迎点击围观

两年前我开始使用 Docker 容器。您可以在这里找到我的 dockerfile 示例:https: //github.com/komljen/dockerfile-examples

这是我的测试场地,但在我开始使用 Docker 进行企业级应用程序部署后,我发现我做错了很多事情。其中之一是我如何在 Docker 中启动应用程序。

几乎我所有的 dockerfiles 在最后都有一些 bash 脚本来在容器内做一些小的改变并最终启动应用程序。我通常将此脚本添加到 Dockerfile 中的 CMD 指令中。我认为这没有任何问题,只是 Docker 停止了它应该的工作。

问题是,当您运行 bash 脚本时,它将获得 PID 1,而您的应用程序是一个 PPID 为 1 的子进程。当您运行 docker stop 时,Bash 不会将 SIGTERM 信号转发给您的应用程序。相反,容器将在 10 秒超时后被终止,这是 docker stop 命令的默认设置。这个超时是可以调整的。

有一种简单的方法可以使用 bash 脚本中的“exec”命令来处理这个问题。它将在不创建新进程的情况下替换 shell,并且您的应用程序将获得 PID 1。让我们先测试这两种情况。

出于测试目的,我将使用这个简单的 redis dockerfile:


 FROM ubuntu:trusty

ENV DEBIAN_FRONTEND noninteractive

RUN
apt-get update &&
apt-get -y install
software-properties-common &&
add-apt-repository -y ppa:chris-lea/redis-server &&
apt-get update &&
apt-get -y install
redis-server &&
rm -rf /var/lib/apt/lists/*

COPY start.sh start.sh

EXPOSE 6379 RUN rm /usr/sbin/policy-rc.d CMD ["/start.sh"]

这是 start.sh 脚本,它将更改一些推荐的 redis 容器内核设置(docker 必须在特权设置为 true 的情况下启动):


 FROM ubuntu:trusty

ENV DEBIAN_FRONTEND noninteractive

RUN
apt-get update &&
apt-get -y install
software-properties-common &&
add-apt-repository -y ppa:chris-lea/redis-server &&
apt-get update &&
apt-get -y install
redis-server &&
rm -rf /var/lib/apt/lists/*

COPY start.sh start.sh

EXPOSE 6379 RUN rm /usr/sbin/policy-rc.d CMD ["/start.sh"]

现在让我们构建并运行这个容器:


 FROM ubuntu:trusty

ENV DEBIAN_FRONTEND noninteractive

RUN
apt-get update &&
apt-get -y install
software-properties-common &&
add-apt-repository -y ppa:chris-lea/redis-server &&
apt-get update &&
apt-get -y install
redis-server &&
rm -rf /var/lib/apt/lists/*

COPY start.sh start.sh

EXPOSE 6379 RUN rm /usr/sbin/policy-rc.d CMD ["/start.sh"]

然后查看redis容器里面运行的是什么:


 FROM ubuntu:trusty

ENV DEBIAN_FRONTEND noninteractive

RUN
apt-get update &&
apt-get -y install
software-properties-common &&
add-apt-repository -y ppa:chris-lea/redis-server &&
apt-get update &&
apt-get -y install
redis-server &&
rm -rf /var/lib/apt/lists/*

COPY start.sh start.sh

EXPOSE 6379 RUN rm /usr/sbin/policy-rc.d CMD ["/start.sh"]

好的,所以这是一个问题。现在让我们尝试停止这个 docker 容器:


 FROM ubuntu:trusty

ENV DEBIAN_FRONTEND noninteractive

RUN
apt-get update &&
apt-get -y install
software-properties-common &&
add-apt-repository -y ppa:chris-lea/redis-server &&
apt-get update &&
apt-get -y install
redis-server &&
rm -rf /var/lib/apt/lists/*

COPY start.sh start.sh

EXPOSE 6379 RUN rm /usr/sbin/policy-rc.d CMD ["/start.sh"]

10 秒后容器被杀死,没有正常停止。如果我们检查日志,我们可以看到这一点。最后一条消息是 redis 已准备好接受连接:


 FROM ubuntu:trusty

ENV DEBIAN_FRONTEND noninteractive

RUN
apt-get update &&
apt-get -y install
software-properties-common &&
add-apt-repository -y ppa:chris-lea/redis-server &&
apt-get update &&
apt-get -y install
redis-server &&
rm -rf /var/lib/apt/lists/*

COPY start.sh start.sh

EXPOSE 6379 RUN rm /usr/sbin/policy-rc.d CMD ["/start.sh"]

为了让它工作,我们需要更改 start.sh 脚本中的最后一行并重建图像。我们在 /usr/bin/redis-server 命令之前添加 exec:


 FROM ubuntu:trusty

ENV DEBIAN_FRONTEND noninteractive

RUN
apt-get update &&
apt-get -y install
software-properties-common &&
add-apt-repository -y ppa:chris-lea/redis-server &&
apt-get update &&
apt-get -y install
redis-server &&
rm -rf /var/lib/apt/lists/*

COPY start.sh start.sh

EXPOSE 6379 RUN rm /usr/sbin/policy-rc.d CMD ["/start.sh"]

再次构建、启动并运行 ps -ef 命令:


 FROM ubuntu:trusty

ENV DEBIAN_FRONTEND noninteractive

RUN
apt-get update &&
apt-get -y install
software-properties-common &&
add-apt-repository -y ppa:chris-lea/redis-server &&
apt-get update &&
apt-get -y install
redis-server &&
rm -rf /var/lib/apt/lists/*

COPY start.sh start.sh

EXPOSE 6379 RUN rm /usr/sbin/policy-rc.d CMD ["/start.sh"]

如您所见,redis 现在以 PID 1 运行,并且 docker stop 可以正常工作。我们先试试看,再查看docker日志。您应该在 redis 日志中看到此消息:


 FROM ubuntu:trusty

ENV DEBIAN_FRONTEND noninteractive

RUN
apt-get update &&
apt-get -y install
software-properties-common &&
add-apt-repository -y ppa:chris-lea/redis-server &&
apt-get update &&
apt-get -y install
redis-server &&
rm -rf /var/lib/apt/lists/*

COPY start.sh start.sh

EXPOSE 6379 RUN rm /usr/sbin/policy-rc.d CMD ["/start.sh"]

这就是我将 exec 与 postgres 和 tomcat 容器一起使用的方式,其中进程不以 root 用户运行:


 FROM ubuntu:trusty

ENV DEBIAN_FRONTEND noninteractive

RUN
apt-get update &&
apt-get -y install
software-properties-common &&
add-apt-repository -y ppa:chris-lea/redis-server &&
apt-get update &&
apt-get -y install
redis-server &&
rm -rf /var/lib/apt/lists/*

COPY start.sh start.sh

EXPOSE 6379 RUN rm /usr/sbin/policy-rc.d CMD ["/start.sh"]

由于 sudo 或 su,这里的进程不会以 PID 1 运行,但是 Docker stop 在这两种情况下都能完美运行。这样做的原因是 sudo 和 su 命令会将 SIGTERM 信号传递给子进程。