我能在一个Docker容器中运行多个程序吗?

193

我试图从部署用户桌面应用程序的角度了解Docker。我的应用程序只是一个Flask Web应用程序和Mongo数据库。通常,我会在虚拟机中安装它们,并将主机端口转发到客户机Web应用程序上。 我想尝试使用Docker,但我不确定如何使用多个程序。文档说只能有一个ENTRYPOINT,那么我该如何同时运行Mongo和我的Flask应用程序呢?还是它们需要在单独的容器中,那么它们如何相互通信,这样做如何轻松分发应用程序?


2
非常准确:让我想知道为什么Docker这么受欢迎...(单进程..?) - 但让我们看看答案告诉我们什么.. - WestCoastProjects
10个回答

138

只能有一个ENTRYPOINT,但通常该目标是启动所需程序的脚本。您还可以使用例如Supervisord或类似工具来负责在单个容器内启动多个服务。 这是一个运行mysql、apache和wordpress的Docker容器的示例

假设您有一个数据库仅由一个Web应用程序使用,则将它们都放在单个容器中可能更容易。

如果您有一个被多个应用程序共享的数据库,则最好将数据库放在自己的容器中,每个应用程序都在自己的容器中。

当应用程序在不同的容器中运行时,至少有两种可能性可以使它们彼此通信:

  1. 使用公开的IP端口并通过它们进行连接。
  2. 最近的Docker版本支持链接

3
看起来Docker的新版本现在支持Docker容器网络 - jpierson
Docker现在支持运行Supervisor,允许您为每个进程指定行为,例如autorestart=true、stdout_logfile、stderr_logfile等。请查看https://docs.docker.com/engine/admin/using_supervisord/。 - Andreas Lundgren
9
在这个例子中,我绝对不建议尝试在同一个容器中运行Web应用程序和MongoDB。Docker中的supervisord或类似的init进程有很好的用例,但这个例子不属于它们的范畴。使用docker-compose在单独的容器中运行这两个服务要简单得多。 - nicolas-van
@nicolas-van 为什么更简单?是因为如果数据库出现问题,我可以重新启动数据库容器而不必重新启动整个系统吗? - brillout
同一台机器上的应用程序也可以通过Unix域套接字进行通信。保证最高性能。 - Sceptical Jule

34

我强烈反对之前一些解决方案的建议,即在同一个容器中运行两个服务。文档中明确指出这不是推荐做法

通常建议使用每个容器一个服务来分离关注点。该服务可以分叉成多个进程(例如,Apache Web服务器启动多个工作进程)。拥有多个进程是可以的,但为了从Docker中获得最大的好处,避免一个容器负责您整个应用程序的多个方面。您可以使用用户定义的网络和共享卷连接多个容器。

supervisord或类似程序有很好的用例,但运行Web应用程序+数据库不属于其中之一。

您应该绝对使用docker-compose来完成这项任务,并编排具有不同职责的多个容器。

更多关于为什么将多个服务拆分到不同容器中更好的解释

由于在这一点上存在明显的误解,我将进行更全面的解释。

为了达到这个目的,我将使用一个更复杂的部署示例,因为 Flask 应用程序 + MongoDB 数据库有点太简单了,无法展示现代 Web 应用程序开发中的真正复杂性。让我们想象一下我们有:
  • 使用 Flask 的 Python Web 服务器
  • MongoDB 服务器(可以是任何其他数据库,如 Postgresl、mariadb 等)
  • 作为共享缓存的 Redis 服务器(可以是类似的解决方案,如 Memcached)
  • 作为消息队列的 Rabbit MQ 服务器(可以是任何其他消息队列)
  • 另一个 Python 应用程序,将在 Rabbit MQ 服务器中处理消息
  • Caddy 实例作为反向代理和 SSL 提供者(可以是 Apache、Nginx 等)

所以我们有 6 种类型的服务。这种部署方式在现代 Web 开发中并不新鲜或特别,因为大多数真实世界的专业项目往往至少具有这种复杂性(通常更多)。

Docker 允许我们在这里做什么,以及文档中明确说明的推荐使用方式,是将这 6 个服务中的每一个分别放入独立的容器中。

以下是我们更喜欢这样做的原因:

依赖关系分离

每个服务都有自己的依赖关系。至少,它们都直接或间接地依赖于系统的libc,即使这种依赖有时也会出现问题(某些程序被打包使用较新版本的C库)。它们还肯定会有其他与系统相关的依赖项。这可以是低级别的C库,如openssl、libxml、libevent等,也���以是更高级别的工具,如Python、nodejs、ruby等。而且,每个服务只支持每个依赖项的一定范围版本。

虽然您“可以”尝试在单个容器中安装每个服务,但这将比您想象的更麻烦。您可能必须依赖于镜像的基本发行版软件包管理器,这意味着对每个服务强制执行特定版本。例如,您无法选择在给定Ubuntu版本上默认安装的postgresql或redis版本。您还可以通过使用第三方存储库安装所需的特定版本,但在大多数Linux / Unix系统上,这通常会遇到依赖项不兼容性问题。

很不幸,现在被固定到特定版本的这些服务并不是一个令人满意的解决方案。也许您想使用更新版本的Postgresql版本,因为该版本中有新功能,您想在Python程序中使用它们。或者由于openssl实现中存在安全问题需要快速升级,您可能需要升级Caddy实例。

试图将您使用的每个服务的特定版本放入单个容器中,迟早会成为与在任何Linux机器上尝试一起安装这些服务相同的地狱。

Docker允许通过允许每个服务都拥有其自己的依赖关系,彼此完全独立,来解决这个问题。更改任何这些服务之一的版本通常只需要更改docker-compose.yml文件中的一行配置。这是巨大的优势和使用Docker的许多原因之一。

不同的扩展方式

假设你已经成功部署了这6个服务,现在你的Web项目正在运行。现在你有一个新问题:真实用户正在使用你的应用程序。这些真实用户使用资源,包括CPU、RAM、磁盘IO、网络IO、存储空间等,所有这些都受到部署机器的限制。你现在需要能够扩展你的应用程序。

要想扩展应用程序,你不能只是复制整个堆栈。对于Python flask应用程序,你可能可以横向扩展(如果它是无状态的,就像应该的那样),但MongoDB实例的扩展完全不同,并需要特定的配置。如果你扩展了Python应用程序,你可能需要适应你的caddy实例来实现负载均衡。你可能不会扩展你的redis实例,因为它是一个内存缓存服务器。也许你只需要扩展后台Python应用程序处理消息队列中的消息,因为你发现这是应用程序中最消耗CPU的部分。等等...

Docker通过将每个服务定义为单独的容器,使得这种扩展和配置适应更加容易。在最好的情况下(不是全部情况但很多情况下),您需要做的唯一适应是调整Docker配置,而不会对镜像产生任何影响。在多台计算机上扩展部署也将大大简化,可能需要使用更复杂的编排工具(如Kubernetes),因为使用docker-compose存在一定的限制,但至少您将有解决方案。

不同的存储方式

我们这里有一个数据库,我们的MongoDB实例。像所有数据库一样,它需要特殊的考虑因素,这些考虑因素在无状态服务的背景下不存在。我们说的是数据持久性、备份、档案、数据迁移...这些小事情并不那么容易处理,但即使是最简单的实际用例也是100%必要的。

为了处理这些考虑因素,在Docker出现之前的旧日常规是强制规定数据库位于运行应用程序代码的不同计算机上。这种做法即使在Docker可用的情况下仍然存在,因为仍然有充分的理由完全独立地存储数据。

但是对于那些想在数据库中使用Docker的人来说,即使只是在开发阶段,将它们系统地放在独立的容器中仍然是最好的选择。这将允许以明显独立的方式管理数据库的生命周期和存储,避免与其他任何东西产生任何不必要且可能不需要的交互。

您还可以选择在开发中使用Docker部署数据库,以便于使用,但在生产环境中使用像AWS RDS这样的东西(因此不使用Docker)。这也可以轻松完成,只需使用不同的配置,而不会对其他服务产生任何副作用。

结论

在学习如何使用Docker时,有两个不同的方面必须掌握:

  • 学习如何创建新的Docker镜像。这需要学习如何创建Dockerfile文件,以及关于sh/bash脚本的许多知识。
  • 学习如何编排现有的Docker容器,以便能够真正地利用Docker进行实际用例。通常我们首先学习docker-compose,因为它可能是最简单的工具,但Kubernetes似乎越来越多地用于此(它也更复杂)。

这些技能有点分离,但也紧密交织在一起。你需要知道Docker容器的工作原理才能编排它们,而为此你需要能够创建Docker镜像。但是要创建良好的Docker镜像,你还需要了解它们的最终目标,即能够轻松地将它们编排到多个容器中。

因此,鉴于最初的问题,给出的最佳建议是不要尝试为Python flask应用程序+MongoDB数据库创建单个镜像。最好的建议是创建一个仅包含Python flask应用程序的Docker镜像,并创建一个docker-compose.yml文件,其中包含Python flask应用程序和MongoDB数据库(重用官方MongoDB镜像之一)。

当然,这将需要学习使用docker-compose,学习配置官方MongoDB镜像,并可能对Python flask应用程序进行一些适应,以通过环境变量注入一些配置参数。但是这些必要性远非浪费时间。这些是使用Docker的“正确”方式的完整部分,以实现其真正的有用性。


8
这是一条评论,而不是答案。请考虑添加解释和/或链接来支持这个立场。否则它就没有帮助。 - Ivan Ivanov
9
这是一个答案,我的最佳建议是在这种情况下使用docker-compose。无论如何,你说得对,我可以提供更多官方推荐的链接。我会更新的。 - nicolas-van
2
问题是关于在一个容器中运行2个进程,因此不需要关心最佳实践。我会给您一个例子:我必须在基于PhotonOS的镜像中运行rabbitmq和一个Java进程...所以我使用了一个入口脚本,并将其用作ENTRYPOINT :) - Vallerious
1
原始问题并不是关于在Docker容器中运行两个进程的技术可行性的通用问题。它阐述了一个特定的用例,即部署Python应用程序以及MongoDB数据库。对于这种情况,最好的建议是不鼓励使用单个容器,而是推荐使用docker-compose。 - nicolas-van
6
这应该是答案。 "我正在努力理解Docker"。很明显,这是一个新的Docker用户不理解容器的概念,并试图像对待标准Web服务器一样对待容器,许多新手在使用Docker时都会尝试做到这一点(我也是其中之一)。指向文档并解释最佳做法是一个好答案。 - Joe
显示剩余3条评论

24

我有一个类似的需求,需要运行 LAMP 堆栈、Mongo DB 和我的自定义服务。

Docker 是基于操作系统的虚拟化技术,因此它会将容器与正在运行的进程隔离开来,因此至少需要一个前台进程在运行中。

因此,您需要提供自己的启动脚本作为入口点,这样您的启动脚本就成为了扩展的 Docker 镜像脚本,您可以在其中堆叠任意数量的服务,只要至少有一个前台服务已启动,而且放在最后

因此,我的 Docker 镜像文件在最后面有以下两行:

COPY myStartupScript.sh /usr/local/myscripts/myStartupScript.sh
CMD ["/bin/bash", "/usr/local/myscripts/myStartupScript.sh"]

我的脚本中运行了MySQL、MongoDB、Tomcat等等。最后我将Apache作为前台线程运行。

source /etc/apache2/envvars
/usr/sbin/apache2 -DFOREGROUND

这使我能够启动所有的服务并保持容器处于活动状态,最后启动的服务将在前台运行。

希望对您有所帮助。

更新:自我上次回答这个问题以来,出现了新的东西,比如Docker Compose,它可以帮助您将每个服务都运行在自己的容器中,并将它们绑定在一起作为这些服务之间的依赖关系,请尝试了解更多关于docker-compose的内容并使用它,除非您的需求与之不符。


17

虽然不建议这样做,但是可以使用wait来在前台运行两个进程。只需创建一个包含以下内容的bash脚本,例如start.sh:

# runs 2 commands simultaneously:

mongod & # your first application
P1=$!
python script.py & # your second application
P2=$!
wait $P1 $P2

在你的Dockerfile中,使用以下内容开始

CMD bash start.sh

如果您想同时运行多个进程,我建议设置本地Kubernetes集群。您可以通过提供一个简单的Kubernetes清单文件来"分发"应用程序。


清晰易懂,希望没有缺陷。我想知道为什么Docker官网没有提到这种方法。 - Sida Zhou
仅此方案适用于我的情况。谢谢。 - Muhammad Usman
两个进程仍在后台运行。 - ns15
@SidaZhou 它确实可以:https://docs.docker.com/config/containers/multi-service_container/ - Lenormju

7
他们可以在单独的容器中,如果应用程序也打算在更大的环境中运行,它们可能会被分开。多容器系统需要一些编排来启动所有所需的依赖项,尽管在Docker v0.6.5+中,有一个内置于Docker本身的新设施Linking来帮助处理这个问题。对于多机解决方案,不过仍然需要从Docker环境外安排一些东西。使用两个不同的容器,两个部分仍通过TCP/IP通信,但除非特别锁定了端口(不建议这样做,因为您将无法运行多个副本),否则必须将MongoDB公开的新端口传递给应用程序,以便它能够与MongoDB通信。同样,Linking可以提供帮助。

对于一个更简单、小型的安装,所有依赖项都在同一个容器中,由最初被调用为ENTRYPOINT的程序启动数据库和Python运行时也是可能的。这可以是一个简单的shell脚本,或者其他进程控制器 - Supervisord非常流行,在公共Dockerfiles中存在许多示例。


6
Docker提供了几个示例来说明如何实现。轻量级的选项是: 将所有命令放入包含测试和调试信息的包装脚本中。将包装脚本作为CMD运行。这是一个非常朴素的例子。首先,包装脚本:
#!/bin/bash

# Start the first process
./my_first_process -D
status=$?
if [ $status -ne 0 ]; then
  echo "Failed to start my_first_process: $status"
  exit $status
fi

# Start the second process
./my_second_process -D
status=$?
if [ $status -ne 0 ]; then
  echo "Failed to start my_second_process: $status"
  exit $status
fi

# Naive check runs checks once a minute to see if either of the processes exited.
# This illustrates part of the heavy lifting you need to do if you want to run
# more than one service in a container. The container will exit with an error
# if it detects that either of the processes has exited.
# Otherwise it will loop forever, waking up every 60 seconds

while /bin/true; do
  ps aux |grep my_first_process |grep -q -v grep
  PROCESS_1_STATUS=$?
  ps aux |grep my_second_process |grep -q -v grep
  PROCESS_2_STATUS=$?
  # If the greps above find anything, they will exit with 0 status
  # If they are not both 0, then something is wrong
  if [ $PROCESS_1_STATUS -ne 0 -o $PROCESS_2_STATUS -ne 0 ]; then
    echo "One of the processes has already exited."
    exit -1
  fi
  sleep 60
done

接下来是 Dockerfile 文件:
FROM ubuntu:latest
COPY my_first_process my_first_process
COPY my_second_process my_second_process
COPY my_wrapper_script.sh my_wrapper_script.sh
CMD ./my_wrapper_script.sh

3
我同意其他答案,使用两个容器是更可取的,但如果您一心想将多个服务捆绑在单个容器中,可以使用类似supervisord的东西。

例如,在Hipache中,包含的Dockerfile运行supervisord,文件supervisord.conf指定hipache和redis-server都要运行。


1
如果专用脚本看起来过于繁琐,你可以使用sh -c显式地生成单独的进程。例如:
CMD sh -c 'mini_httpd -C /my/config -D &' \
 && ./content_computing_loop

0
在Docker中,有两种方法可以运行程序:
  1. CMD
  2. ENTRYPOINT
如果您想了解它们之间的区别,请参考这里
在CMD/ENTRYPOINT中,有两种格式来运行命令:
  • SHELL格式
  • EXEC格式

SHELL格式:

CMD executable_first arg1; executable_second arg1 arg2
ENTRYPOINT executable_first arg1; executable_second arg1 arg2

该版本将创建一个shell并执行上述命令。您可以使用任何shell语法,例如“;”、“&”、“|”等。因此,您可以在此运行任意数量的命令。如果您有一组复杂的命令需要运行,则可以创建单独的shell脚本并使用它。

CMD my_script.sh arg1
ENTRYPOINT my_script.sh arg1

EXEC 格式:

CMD ["executable", "parameter 1", "parameter 2", …]
ENTRYPOINT ["executable", "parameter 1", "parameter 2", …]

在这里,您可以注意到只有第一个参数是可执行文件。从第二个参数开始,一切都成为该可执行文件的参数。

要以EXEC格式运行多个命令

CMD ["/bin/sh", "-c", "executable_first arg1; executable_second"]
CMD ["/bin/sh", "-c", "executable_first arg1; executable_second"]

在上述命令中,我们使用了 shell 命令作为可执行文件来运行该命令。这是以 EXEC 格式运行多个命令的唯一方式。
以下是错误的:
CMD ["executable_first parameter", "executable_second parameter"]
ENTRYPOINT ["executable_first parameter", "executable_second parameter"]

CMD ["executable_first", "parameter", ";", "executable_second",  "parameter"]
ENTRYPOINT ["executable_first", "parameter", ";", "executable_second", "parameter"]

0
我可以在Docker容器中运行多个程序吗?
可以,但存在重大风险。

以下是与上面相同的答案。但是带有详细信息和推荐的解决方案。如果您对此感兴趣。

不推荐

警告。尽管如此,使用相同的容器来运行多个服务并不受 Docker 社区的推荐。Docker 文档中写道:“通常建议您通过每个容器使用一项服务来分隔关注点。”源网址为:

https://archive.ph/3Roa6#selection-307.2-307.100

https://docs.docker.com/config/containers/multi-service_container/

如果您选择忽略上述建议,则您的容器可能会面临更弱的安全性、更不稳定以及未来的痛苦增长。

如果您能够接受以上风险,则使用一个容器来运行多个服务的文档位于:

https://archive.ph/3Roa6#selection-335.0-691.1

https://docs.docker.com/config/containers/multi-service_container/

推荐

如果您需要更强的安全性、更稳定的容器以及未来更大规模和更好的性能,则Docker社区建议采取以下两个步骤:

  1. 每个Docker容器只使用一个服务。最终结果是您将拥有多个容器。

  2. 使用Docker "Networking"功能,按照您的喜好连接任何这些容器。


我从来没有真正理解为什么会这样,特别是对于本地开发容器而言。有些项目变得如此失控,以至于您会有30个容器正在运行,整个项目的性能就像一只完全不受控制的狗。其中一个容器会更易于管理。 - Ryan Mann

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接