Rails 服务器无法启动?
学习如何解决 Rails 服务器无法启动的错误。
Rails tmp/pids/server.pid 未被清理
哎呀,这可真尴尬。不幸的是,Compose 存在一些令人恼火的问题。由于你可能在前面的章节中遇到过这些问题,或者在今后使用 Compose 时也会遇到,因此忽略它们是不负责任的。接下来我们将逐一快速查看这些问题。
幸运的是,我们可以通过一些努力来绕过第一个问题。不知为何,有时在使用 Compose 终止应用(按下 Ctrl+C)后,Rails 服务器似乎无法干净地关闭,导致 Rails 存放在 tmp/pids/ 目录中的 server.pid 文件未被删除。这意味着再次启动应用时,你可能会在输出中看到如下错误:
1$ docker-compose up 2... 3A server is already running. Check /usr/src/app/tmp/pids/server.pid 4...
pid 文件的存在会让 Rails 服务器误以为已有服务器在运行,因此不会启动。Rails 将 server.pid 文件保存在 tmp/pids 目录中。由于我们将本地应用目录挂载到了容器中,该文件最终会出现在我们本地机器的 tmp/pids/ 目录下,并持续存在直到我们手动删除它。
如何解决这个问题?
第一个解决方案
由于我们已将应用目录挂载到容器中,手动删除 server.pid 文件非常简单:
1$ rm tmp/pids/server.pid
然后重新启动 Rails 服务器:
1$ docker-compose up
此时 Rails 应用将正常运行。
第二个解决方案
然而,如果这个问题反复发生,上述步骤并不能真正解决问题。幸运的是,我们可以设置一个巧妙的绕行方案。
让我们看看这个解决方案,然后再进行讨论:
- 我们需要在 Rails 项目根目录下创建一个新文件 docker-entrypoint.sh,内容如下:
1#!/bin/sh 2set -e 3 4if [ -f tmp/pids/server.pid ]; then 5 rm tmp/pids/server.pid 6fi 7 8exec "$@"
- 使该文件可执行:
1$ chmod +x docker-entrypoint.sh
- 在我们的 Dockerfile 中,于最后一个
CMD指令之前添加以下一行,指定ENTRYPOINT指令:
1ENTRYPOINT ["./docker-entrypoint.sh"]
- 停止、重建并重启 web 服务:
1$ docker-compose stop web 2$ docker-compose build web 3$ docker-compose up -d web
🍀 实践
让我们尝试完成以下操作:
- 编辑 docker-entrypoint.sh。
- 点击 Run 按钮。
- 仅启动
web服务。(不要使用-d选项)。 - 退出应用。
- 验证文件 server.pid 是否已被删除。
注意: 当你看到提示 "You may begin now..." 后,请立即启动
web服务。如果你对文件做了任何修改,请点击 Run 按钮。
No configuration found for ID: fc4383ee81db919e4aa06b0c1971610fe33b16b34daf067abfe8eea28ba486ed
解决方案如下:
-
按如下方式编辑
docker-entrypoint.sh:1#!/bin/sh 2set -e 3 4if [ -f tmp/pids/server.pid ]; then 5 rm tmp/pids/server.pid 6fi 7 8exec "$@" -
点击 Run 按钮。
-
使用以下命令仅启动
web服务:1docker-compose up web -
使用
Ctrl+C退出应用。 -
为验证文件 server.pid 是否已被删除,请执行以下操作:
1cat tmp/pids/server.pid如果收到如下错误,则说明
server.pid已被删除,且我们的docker-entrypoint.sh工作正常。1cat: tmp/pids/server.pid: No such file or directory
注意: 如果你的
.sh文件格式有误,可能会遇到类似以下错误:1web_1 | standard_init_linux.go:219: exec user process caused: exec format error 2myapp_web_1 exited with code 1出现这种情况时,请修正
.sh文件并点击 Run 按钮。再次启动web服务。如果.sh文件正确,将能成功启动。
解决方案:
-
按如下方式编辑 docker-entrypoint.sh:
1#!/bin/sh 2set -e 3if [ -f tmp/pids/server.pid ]; then 4rm tmp/pids/server.pid 5fi 6exec "$@" -
点击 Run 按钮。
-
仅使用以下命令启动
web服务:1docker-compose up web -
使用
Ctrl+C退出应用。 -
为验证文件 server.pid 是否已被删除,请执行以下操作:
1cat tmp/pids/server.pid如果收到以下错误,说明
server.pid已被删除,且我们的docker-entrypoint.sh正常工作。1cat: tmp/pids/server.pid: No such file or directory
注意: 如果你的
.sh文件格式有误,可能会遇到类似以下错误:1web_1 | standard_init_linux.go:219: exec user process caused: exec format error 2myapp_web_1 exited with code 1在这种情况下,请修正你的
.sh文件并点击 Run 按钮。再次启动web服务。如果.sh文件正确,将能成功启动。
那么这一切到底在做什么?
入口点(Entrypoint) 是在启动新容器时附加到运行命令前的指令。在我们的情况中,已将 ./docker-entrypoint.sh 设置为 web 服务的 ENTRYPOINT。这意味着,当我们启动一个新的 web 容器时,不再只是简单地运行默认命令:
1bin/rails s -b 0.0.0.0
而是会实际运行这个命令,得益于我们新增的 ENTRYPOINT 指令:
1./docker-entrypoint.sh bin/rails s -b 0.0.0.0
由于这个 shell 脚本会被执行,我们需要像第 2 步那样赋予其执行权限。
那么我们的 docker-entrypoint.sh shell 脚本到底在做什么?
如果你不熟悉 Bash,我们来逐行快速解析一下:
1#!/bin/sh 2set -e 3 4if [ -f tmp/pids/server.pid ]; then 5 rm tmp/pids/server.pid 6fi 7 8exec "$@"
-
在 Bash 脚本中,最佳实践是在开头加上
set -e(第 2 行)——这使得脚本在后续任意命令出错(非零退出状态)时立即失败。 -
第 4 行的 if 语句用于检查
tmp/pids/server.pid文件是否存在。如果存在,则在第 5 行将其删除。这是脚本的清理部分,确保即使留下server.pid文件,我们的 Rails 服务器也能始终正常启动。 -
然而,最终我们希望容器启动的是 Rails 服务器,而不是这个 Bash 脚本。这就是第 8 行
exec命令的作用。它表示:“用接下来要运行的程序替换当前正在运行的进程(这个 Bash 脚本)”——几乎就像这个 shell 脚本从未存在过一样。
但 exec 到底运行了哪个程序?
$@ 的意思是“传递给此 Bash 脚本的所有参数”,在我们的情况下就是 bin/rails s -b 0.0.0.0。因此,我们实际上是在说:“用 Rails 服务器替换当前运行的 Bash 脚本”。
总结
总而言之,docker-entrypoint.sh 作为一个包装脚本,为我们提供了一个机会,在清理 pid 文件后,再像什么都没发生一样启动 Rails 服务器。现在你可以放心地随意运行 docker-compose up,无需担心这个烦人的 bug 影响你。
入口点(Entrypoints),尤其是遵循这种模式的,是你工具箱中一个非常有用的工具。你可能会发现其他创造性的用途。同时也要知道,使用 ENTRYPOINT 指令,你也可以直接在 Dockerfile 中指定入口点。