使用Docker Compose创建生产准备的Web应用程序的最佳实践

2021-06-03 23:25:33

这里有几种模式的一些模式。自2014年以来,ve over of o' ve自2014年以来使用码头。我从做一堆自由工作中提取了这些。

5月27日,2021年我给了Dockercon 21的Live Demo。这是一个29分钟的谈话,我涵盖了一堆Docker Compose和Dockerfile模式,在开发和部署Web应用程序时一直在使用和调整多年。

这是一大1大实时演示,在那里,我们看看这些模式应用于多服务烧瓶应用程序,但我也使用不同的Web框架(即将在此,更多地参考不同语言编写的其他示例应用程序。

如果您喜欢视频而不是阅读,那就是YouTube的视频,其中包含时间戳。这是导演的削减,由于缺乏时间,必须从Dockercon切出4分钟内容。

这篇文章是视频的书面形式。谈话对某些主题进行了更详细的详细信息,但我偶尔于书面形式的某些主题扩展,因为即使是导演的剪辑版本在我录制的时候按下时间。

作为免责声明,这些都是个人意见。我不是想说我所做的一切都是完美的,但我会说所有这些东西都在迄今为止的发展和生产中都非常好,这是我个人和客户的项目。

大多数模式与任何语言和Web框架完全相同,我只想快速提及我在将示例应用程序放在一起为一群语言和框架。

所有这些都将一些共同的服务汇集,如运行Web应用程序,背景工人(如果适用),PostgreSQL,Redis和WebPack。

至于游戏的例子,我想给Lexie喊出巨大的喊叫。

她是一个主要与Scala合作的软件工程师,通过纯粹的运气,我们最终进入了关于与Docker无关的内容联系。长话短说,几对编程会话后它已准备好了。没有她的帮助和专业知识,没有任何播放示例应用程序可能存在。

让我们在开发和生产中使用Docker撰写的一些模式,提示和最佳实践开始。

Docker撰写了折除版本属性的规范提及,它只在规范中定义,以便向后兼容。它只是信息。

在此之前,它是常见的,定义版本:" 3.8"或者,无论您想要的API版本如何,因为它控制了哪些属性可用。使用Docker Compose V1.27 +您可以将其全部放在一起,即删除代码!

似乎我们已经完全循环回到码头撰写曾经被称为图1的码头和版本1没有版本定义。

在开发/生产奇偶校验主题上,我喜欢在所有环境中使用相同的docker-compose.yml。但是当您想要在开发中运行某些集装箱但不在生产中时,这会变得有趣。

例如,您可能希望在开发中运行WebPack观察者,但仅在生产中提供捆绑的资产。或者也许您要在生产中使用托管PostgreSQL数据库,但在用于开发的容器中本地运行PostgreSQL。

基本思想是您可以创建该文件并将这样的文件添加到它:

服务:WebPack:构建:上下文:" 。"目标:" webpack" args: - " node_env = $ {node_env:-production}"命令:"纱线运行手表" env_file: - " .env"卷: - " 。:/ app"

它是一个标准的Docker撰写文件,默认情况下运行Docker-Compose Up,Docker Compose会将您的Docker-Compose.yml文件和Docker-Compose.overRide.yml合并为一个运行的1个单元。这会自动发生。

然后,您可以将此覆盖文件添加到.gitignore文件,因此当您将代码推送到生产时(让我们说出已设置的VPS),它不会在那里和Voila,你刚刚创建了一个让它的模式您在DEV中运行的内容,但不在产品中不必重复一堆服务并创建Docker-Compose-Dev.yml + Docker-Compose-Prod.yml文件。

对于开发人员方便,您还可以将Docker-compose.override.yml.example添加到您的repo中,从版本控制中忽略,现在您所要做的就是Cp Docker-compose.override.yml.example docker-compose。 override.yml在克隆项目时使用真正的覆盖文件。这在DEV和CI中都是方便的。

这可以使用yaml的别名和锚点以及obccker组成的扩展字段来完成。我在Docker Tip#82中详细写了一下。

但是这是基本的想法,你可以在你的docker-compose.yml文件中定义它:

X-app:&默认应用程序构建:上下文:" 。"目标:" app" args: - " flask_env = $ {flask_env:-production}" - " node_env = $ {node_env:-production}" depends_on: - " Postgres" - " redis" env_file: - " .env"重启:" $ {docker_restart_policy:-unless-stopped}" stop_grace_period:" 3s" TTY:真正的卷: - " $ {docker_web_volume: - 。/ public:/ app / public}"

网:<< :*默认应用程序端口: - " $ {docker_web_port_forward:-127.0.0.1:8000}:8000"工人:<< :* default-app命令:celery -a" hello.app.celery_app" Worker -l" $ {celery_log_level:-info}"

这将将所有第一个代码片段应用于Web和工作服务。这避免了必须复制那些〜15行的属性。

您还可以覆盖特定服务中的别名属性,允许您自定义它。在上面的例子中,您可以设置stop_grace_period:" 10s"仅仅是工人服务,如果您愿意。它将优先于别名是什么。

在这样的情况下,这种模式特别方便,其中2个服务可能使用相同的Dockerfile和代码库但具有其他小差异。

除此之外,我将为每个主题显示相关的代码行,所以您在每个部分中所看到的内容都不包含一切。您可以查看完整代码示例的GitHub Repos。

总的来说,我尽量不要假设我可以将应用程序部署到。它可以在使用Docker编写的单个vps上,kubernetes群或甚至是heroku。

在所有3个案例中,我将使用Docker,但它们如何运行急剧不同。

这意味着我更喜欢在Docker-Compose.yml文件中定义我的健康检查而不是dockerfile。技术上Kubernetes如果在您的Dockerfile中找到一个,那么它将禁用一个健康检查,因为它有自己的准备检查,但这里的外卖器是我们应该避免潜在的问题。我们不应该依赖于禁用事物的其他工具。

以下是在Docker-compose.yml文件中定义它时,健康检查的样子

网:<< :*默认应用程序健康检查:测试:" $ {docker_web_healthcheck_test:-curl localhost:8000 / up}"区间:" 60s"超时:" 3s" start_period:" 5s"重试:3

有关这种模式的整洁是它允许我们在运行时设置自运行状况检查以来,我们可以调整我们的健康检查VS生产。

这是使用环境变量完成的,我们将很快谈论,但现在的外带正在开发中,我们可以定义一个健康检查,它可以/ bin / true而不是默认的curl localhost:8000 / UP健康检查。

在DEV中的意思我们不会被与每分钟的健康检查触发相关的日志输出。而不是/垃圾箱/真实将运行,这几乎是一个禁忌。这是一个非常快速的运行命令,返回退出代码0,它将使健康检查通过。

在我们进入这一点之前,这里将在我们的代码repo中有一个常见的模式,从版本控制中忽略了一个.env文件。此文件将与在开发和生产之间可能改变之间的任何内容进行秘密组合。

我们还将包含一个.env.example文件,该文件是具有非秘密环境变量的版本控件,以便在开发和CI中,通过将此文件复制到.env,通过cp .env将此文件复制到。示例.env。

#哪些环境正在运行?这些应该是"发展"或"生产" #export flask_env = production #export node_env = Production Export Flask_env = Development Export node_env =开发

对于文档,我喜欢评论默认值是什么。这样它就会被覆盖,我们完全知道它是什么改变。

说到默认值,我试图坚持使用我希望这些值在生产中的内容。这减少了人类错误,因为它在生产中意味着您只需要设置一些环境变量(秘密,少数其他人等)。

在开发中,我们覆盖了多少,因为可以事先在示例文件中设置和配置。

总而言之,当您将环境变量与Docker组合组合并使用DockerFile构建args时,您可以在所有环境中使用相同的代码,同时只更改一些env变量。

回到我们的DEV / PROS奇偶主题的主题我们可以真正利用我们的Docker-Compose.yml文件中的环境变量。

您可以通过设置$ {flask_env}等内容来定义此文件中的环境变量。默认情况下,Docker Compose将在与您的Docker-Compose.yml文件中查找同一位置的.env文件以查找和使用该env Var的值。

设置默认值也是一个好主意,以防它没有定义,您可以使用$ {flask_env:-production}。它使用与shell脚本相同的语法,除了它比shell脚本更有限,因为您无法嵌套变量作为另一个变量的默认值。

以下是利用环境变量的一些常见和有用的方法。

默认情况下,它运行curl命令,但在我们的.env文件中我们可以在开发中设置导出docker_web_healthcheck_test = / bin / true。

如果您想知道为什么我在所有的.env文件中使用导出......如此,它可以在创建项目特定的shell脚本时派上派上使用的其他脚本中的源.env。我在该主题上创建了一个单独的视频。 Docker Compose 1.26+与导出兼容。

在过去我写了关于如何在Docker主机上直接在Docker外部运行Nginx的信息,并使用此模式确保公共Internet上的任何人都无法访问Web的端口。

默认情况下,它仅限于仅允许来自localhost的连接,这是nginx在单个服务器部署中运行的位置。这可以防止互联网上的人们访问example.com:8000,而无需设置云防火墙来阻止Docker在iptables规则中设置的内容。

即使您确实使用受限制端口设置云防火墙,我仍然会这样做。这是另一层安全性,安全性都是关于层。

在dev中,您可以在.env文件中设置导出docker_web_port_forward = 8000,以允许从任何位置的连接。如果您在自我管理的VM中运行Docker而不是使用Docker Desktop或者在本地网络上的多个设备(笔记本电脑,iPad等)上的情况下,那就是友好的。

除非在生产中,除非停止,否则将确保您的容器在重新启动框后会出现,或者如果它们以这样的方式崩溃,则可以通过重新启动进程/容器来恢复它们。

但在开发中,如果您重新启动了Dev Box和您整个生命中创建的每个项目,所以我们可以设置导出docker_restart_policy =否以防止它们自动启动。

如果您计划从在容器中未运行的nginx访问您的静态文件(CSS,JS,图像等),则绑定安装座是合理的选择。

这样,我们只有在我们的公共/目录中的卷安装,它就是那些文件所在的位置。该位置可能是不同的,具体取决于您使用的Web框架以及在大多数示例应用程序中,我尝试使用公共/何时可以使用。

基于您是否计划将上传文件直接转到应用程序中的磁盘,可以是只读或读写寄存器。

但在开发中,您可以设置导出docker_web_volume =。:/ app,因此您可以从具有代码更新中受益,而无需重建您的图像。

如果设置0,则您的服务将使用尽可能多的资源,因为它们需要有效地与未定义这些属性相同。在单个服务器上部署您可能会通过不设置这些,而是​​使用一些技术堆栈来设置,因此可以很重要,例如您使用Elixir。这是因为光束(Erlang VM)将吞噬尽可能多的资源,这可能会干扰您运行的其他服务,例如您的DB等。

虽然即使是单个服务器部署使用任何技术堆栈都可以了解您服务所需的资源是有用的,因为它可以帮助您选择服务器的正确硬件规格,以帮助消除超额付款或提供的服务器。

此外,您将更好地将应用程序部署到Kubernetes或其他容器编排平台。这是因为如果Kubernetes知道您的应用程序使用75MB内存,它就会知道它可以在具有1 GB可用内存的服务器上装配10个副本。

我们围绕Docker组成了很多地面,但现在让我们开启齿轮并谈谈配置应用程序。

使用烧瓶和其他Python基于Python的Web框架,您可能会为您的应用服务器使用Gunicorn或UWSGI。搭配轨道,您可能会使用彪马。无论您的技术堆栈如何,您可能想要为App Server配置一些事情。

我将从烧瓶示例应用程序中显示来自Gunicorn.py文件的示例,但您可以在任何地方应用所有或大多数。

我们将绑定到0.0.0.0,以便您可以从容器外部连接到容器。许多应用程序服务器默认为localhost,在使用Docker时是一个Gotcha,因为它会阻止您能够从您的浏览器上的浏览器连接。这就是为什么这个值很难编码,它不会改变。

当涉及到端口时,我喜欢制作这种可配置的,更重要的是,我选择使用端口作为名称,因为它是Heroku的使用。只要有可能,我会尝试做出可以在广泛的服务上托管的应用程序。在这种情况下,它很容易获胜。

使用Python,Ruby和一些其他语言您的工作者和线程计数控制每秒App服务器可以服务的每秒请求数量。您拥有的越多,您可以使用更多内存和CPU资源的成本处理的并发正常。

类似于端口,Env Vars的命名约定是基于Heroku的名称。

在工人的情况下,它默认为您在主机上拥有的许多VCPU的两倍。这很好,因为它意味着如果您稍后升级服务器,则无需担心更新任何配置,甚至不需要env变量。

但如果要覆盖该值,它仍然可以使用env变量进行配置。

在开发中,我将这两个值设置为1在.env.example中,因为更容易调试在引擎盖下叉的应用程序。您将看到两者都设置为1在具有支持这些选项的应用服务器的示例应用程序中。

某些Web框架和App服务器处理不同方式重新加载的代码。使用枪手,您需要明确地配置Gunicorn来进行代码重新加载。

这是环境变量的主要选择。在这里,我们可以默认为生成虚假,但对于我们的.env文件中的我们的开发环境我们可以将其设置为true。

使用Docker时,日落于stdout而不是磁盘上的文件是一个好主意,因为如果您登录到磁盘,它会在停止并删除容器时立即消失。

相反,如果您登录STDOUT,则可以配置Docker以持续到您的日志,但您会看到合适。您可以登录JournalD,然后使用JournalCtl(非常适合单个服务器部署)浏览您的日志,或者将日志发送到AWS或任何第三方服务上的CloudWatch。

这里的外带是您的所有应用程序都可以登录到STDOUT,然后您可以在1个点的Docker守护程序级别处理日志记录。

pg_user =操作系统。 GetEnv(" postgres_user"" hello")pg_pass = os。 GetEnv(" postgres_password"," password")pg_host = OS。 GetEnv(" postgres_host"," postgres")pg_port = os。 GetEnv(" postgres_port"," 5432")pg_db = os。 getEnv(" postgres_db",pg_user)db = f" postgreSQL:// {pg_user}:{pg_pass} @ {pg_host}:{pg_port} / {pg_db}" sqlalchemy_database_uri =操作系统。 GetEnv(" database_url",db)

以上是配置SQLALCHEMY的特定,但在使用RAILS,DJANGO,PHOENIX或任何其他Web框架时适用相同的概念。

此想法是支持使用与官方PostgreSQL Docker Image期望我们匹配的Postgres_ * env变量,但是最后一行是有趣的,因为它允许我们在Database_URL中使用,这将被使用而不是单个env变量。

现在,我确信您知道PostgreSQL Docker Image期望我们至少设置Postgres_User和Postgres_Password以便工作,但上面的模式允许我们在生产中使用Docker外部的受管数据库以及开发中的本地运行PostgreSQL容器。

我们也可以将其与覆盖文件模式相结合,现在我们得到两个世界的最佳选择。本地开发与在Docker中运行的PostgreSQL本地副本以及您在生产中选择的托管数据库。我用database_url作为名称,因为它是大量托管提供商使用的惯例。

现在配置数据库与在.env文件中更改env变量一样简单。

一个健康的应用程序是一个幸福的应用程序,但严重的是您的申请的健康检查端点是一个很好的想法。

它允许您挂钩自动化工具以在设置的间隔上访问此端点,并在发生异常发生的情况下通知您,例如未获得HTTP状态代码200,甚至不通知您,如果需要很长时间才能获得响应。

上述健康检查如果成功,则返回200,但它还确保应用程序可以连接到PostgreSQL和Redis。 PostgreSQL检查很好,因为它证明您的数据库已启动,您的应用程序可以使用正确的用户/密码登录,您至少读取了DB的读取访问。同样与Redis,这是一个基本的连接测试。

结束到结束,整个端点可能会在少于1毫秒的情况下响应大多数Web框架,因此您的服务器上不会是一个负担。

我真的很喜欢有一个专门的健康检查端点,因为你可以做最小的工作可以获得你想要的结果。例如,您可以点击您的应用程序的主页,但如果您的主页执行8个数据库查询并呈现50KB的HTML,那么如果您的健康检查将每分钟访问该页面的那种浪费。

使用专用的健康入住,现在您可以使用Docker Compose,Kubernetes或外部监控服务,如Uptime机器人使用它。

我和你一起去过URL,因为它很短而描述。在过去我用/健康但在听到DHH(Rails的创建者)后切换到/恢复)提及一次。他真的很擅长享受伟大的名字!

现在让我们开关齿轮并谈谈你的dockerfile。 我们讨论的所有概念都将适用于任何Web框架。 在Dockerfile配置方面,大多数示例应用程序如何,它真的很有趣。 Dockerfile拥有(2)从中的说明。 那是因为有2个不同的阶段。 每个阶段都有一个名称,它是webpack和app。 一世 ......