以零或最短的停机时间更新 Docker 容器

假设您在容器中运行服务,并且通过其 docker 映像提供了新版本的服务。 在这种情况下,您想更新 Docker 容器。

更新 docker 容器不是问题,但在不停机的情况下更新 docker 容器具有挑战性。

使困惑? 让我一一向您展示这两种方式。

方法一:更新docker容器到最新镜像(导致宕机)

该方法基本上由以下步骤组成:

  • 拉取最新的 docker 镜像
  • 停止并移除运行旧 docker 镜像的容器
  • 使用新拉取的 docker 镜像创建一个新容器

想要命令吗? 干得好。

列出 docker 镜像并获取有更新的 docker 镜像。 使用 docker pull 命令获取此映像的最新更改:

docker pull image_name

现在获取运行旧 docker 映像的容器的容器 ID 或名称。 为此,请使用 docker ps 命令。 停止此容器:

docker stop container_ID

并移除容器:

docker rm container_id

下一步是使用与运行前一个容器相同的参数运行一个新容器。 我相信您知道这些参数是什么,因为您首先创建了它们。

docker run --name=container_name [options] docker_image

你看到这种方法的问题了吗? 您必须停止正在运行的容器,然后创建一个新容器。 这将导致正在运行的服务停机。

如果它是一个关键任务项目或高流量的 Web 服务,即使是一分钟的停机时间也可能会产生很大的影响。

想知道解决这个问题的更安全、更好的方法吗? 阅读下一节。

方法 2:在反向代理设置中更新 docker 容器(停机时间为零或最短)

如果您正在寻找一个简单的解决方案,很抱歉让您失望,但它不会是一个,因为在这里您必须使用 Docker Compose 在反向代理架构中部署您的容器。

如果您希望使用 docker 容器管理关键服务,从长远来看,反向代理方法将为您提供很大帮助。

让我列出反向代理设置的三个主要优点:

  • 您可以在同一台服务器上部署多个面向公众的服务。 这里没有端口阻塞。
  • Let’s Encrypt 服务器负责所有服务、所有容器的 SSL 部署。
  • 您可以更新容器而不影响正在运行的服务(对于大多数 Web 服务)。

如果您想了解更多信息,可以查看官方 Nginx 词汇表,其中重点介绍了 常见用途 反向代理及其如何 比较 带有负载均衡器。

我们有一个关于设置 Nginx 反向代理以托管多个在同一服务器上的容器中运行的 Web 服务实例的深入教程。 所以,我不再在这里讨论它。 您应该首先使用此架构设置您的容器。 相信我,这是值得的麻烦。

在本教程中,我设计了一个循序渐进的方法,对您的日常 DevOps 活动非常有帮助。 此要求不仅在您更新容器时非常必要,而且在您希望在不牺牲宝贵的正常运行时间的情况下对任何正在运行的应用程序进行非常必要的更改时也是非常必要的。

从这里开始,我们假设您在反向代理设置下运行您的 Web 应用程序,这将确保在我们将要做的配置更改后,重新路由对新的最新容器按预期工作。

我会先展示这个方法的步骤,然后是一个真实的生活 example.

第 1 步:更新 docker compose 文件

首先,您需要使用最新图像的版本号来编辑现有的 docker compose 文件。 它可以在 码头工人中心,特别是在应用程序的“标签”部分下。

进入应用程序目录并使用命令行文本编辑器编辑 docker-compose 文件。 我在这里使用了 Nano。

[email protected]:~/web-app$ nano docker-compose.yml

之内 services:, 更新 image: web-app:x.x.x 使用最新的版本号并保存文件。

您可能想知道为什么不使用 latest 标签而不是手动指定版本号? 我是故意这样做的,因为我注意到在更新容器时,最新标签在实际获取最新版本的 dockerized 应用程序时可能会出现间歇性延迟。当您直接使用版本号时,您总是可以绝对当然。

第 2 步:扩展新容器

当您使用以下命令时,会根据 docker compose 文件中所做的新更改创建一个新容器。

[email protected]:~/web-app$ docker-compose up -d --scale web-app=2 --no-recreate

请注意,之前的容器仍在运行。 这 --scale 标志用于创建指定的附加容器。 这里, web-app 已设置为 Web 应用程序的服务名称。

即使您指定扩展到 2 个容器, --no-recreate 确保只添加一个,因为您已经运行了旧容器。

了解更多关于 --scale--no-recreate flag,查看官方 docker-compose up 文档 .

第 3 步:移除旧容器

在第 2 步之后,等待大约 15-20 秒以使新更改生效,然后删除旧容器:

[email protected]:~/web-app$ docker rm -f old-web-app

在不同的 Web 应用程序上,运行上述命令后反映的更改在行为上有所不同(在本教程的最底部作为奖励提示进行了讨论)。

第 4 步:像以前一样缩小到单个容器设置

对于最后一步,您再次缩小到单个容器设置:

[email protected]:~/web-app$ docker-compose up -d --scale web-app=1 --no-recreate

我已经用 Ghost、WordPress、Rocket.Chat 和 Nextcloud 实例测试了这种方法。 除了 Nextcloud 切换到维护模式几秒钟外,该过程对其他三个都非常有效。

话语 然而,这是另一个故事,由于它的混合模型,在这种情况下可能是一个非常棘手的例外。

底线是:Web 应用程序在 docker 化时使用标准 docker 实践越多,日常管理所有 Web 应用程序容器就越方便。

现实生活 example: 在不停机的情况下更新实时 Ghost 实例

正如承诺的那样,我将向你展示真实的生活 example. 我将向您展示如何在不停机的情况下将运行在 docker 容器中的 Ghost 更新到较新的版本。

Ghost 是一个 CMS,我们将它用于 Linux 手册。 这 example 这里显示的是我们用来更新运行该网站的 Ghost 实例的内容。

说,我有一个基于旧版本的现有配置,位于 /home/avimanyu/ghost

version: '3.5'
services:
  ghost:
    image: ghost:3.36
    volumes:
      - ghost:/var/lib/ghost/content
    environment:
      - VIRTUAL_HOST=blog.domain.com
      - LETSENCRYPT_HOST=blog.domain.com
      - url=https://blog.domain.com
      - NODE_ENV=production
    restart: always
    networks:
      - net

volumes:
  ghost:
    external: true

networks:
  net:
    external: true

请注意,上述 docker compose 配置基于此处描述的预先存在的 Nginx docker 配置,运行在名为 net. 它的 docker 卷也是手动创建的 docker volume create ghost-blog.

当我检查它时 docker ps

CONTAINER ID        IMAGE                                    COMMAND                  CREATED             STATUS              PORTS                                      NAMES
2df6c27c1fe3        ghost:3.36                             "docker-entrypoint.s…"   9 days ago          Up 7 days           2368/tcp                                   ghost_ghost-blog_1
89a5a7fdcfa4        jrcs/letsencrypt-nginx-proxy-companion   "/bin/bash /app/entr…"   9 days ago          Up 7 days                                                      letsencrypt-helper
90b72e217516        jwilder/nginx-proxy                      "/app/docker-entrypo…"   9 days ago          Up 7 days           0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp   reverse-proxy

在撰写本文时,这是一个较旧版本的 Ghost。 是时候将它更新到最新版本 3.37.1 了! 因此,我在图像部分将其修改为:

version: '3.5'
services:
  ghost-blog:
    image: ghost:3.37.1
    volumes:
      - ghost-blog:/var/lib/ghost/content
    environment:
      - VIRTUAL_HOST=blog.domain.com
      - LETSENCRYPT_HOST=blog.domain.com
      - url=https://blog.domain.com
      - NODE_ENV=production
    restart: always
    networks:
      - net

volumes:
  ghost-blog:
    external: true

networks:
  net:
    external: true

现在充分利用缩放方法:

[email protected]:~/ghost$ docker-compose up -d --scale ghost-blog=2 --no-recreate

使用上述命令,旧容器不受影响,但新容器使用相同的配置加入,但基于最新版本的 Ghost:

[email protected]:~/ghost$ docker-compose up -d --scale ghost-blog=2 --no-recreate
Pulling ghost (ghost:3.37.1)...
3.37.1: Pulling from library/ghost
bb79b6b2107f: Already exists
99ce436c3449: Already exists
f7bdc31da5f5: Already exists
7a1300b9ff59: Already exists
a495c68fa838: Already exists
6e362a39ec35: Already exists
b68b4f3c36f7: Already exists
41f8b02d4a71: Pull complete
3ecc736ea4e5: Pull complete
Digest: sha256:595c759980cd22e99037811397012908d89efb799776db222a4be6d4d892917c
Status: Downloaded newer image for ghost:3.37.1
Starting ghost_ghost-blog_1 ... done
Creating ghost_ghost-blog_2 ... done

如果我使用传统的方法 docker-compose up -d 相反,我无法避免基于 Ghost 的最新映像重新创建现有容器。

娱乐包括移除旧容器并在其位置创建一个具有相同设置的新容器。 这是发生停机并且站点变得无法访问的时候。

这就是为什么你应该使用 --no-recreate 放大时标记。

所以现在我有两个基于相同的 ghost 配置运行的容器。 这是我们避免停机的关键部分:

[email protected]:~/ghost$ docker ps
CONTAINER ID        IMAGE                                    COMMAND                  CREATED             STATUS              PORTS                                      NAMES
f239f677de54        ghost:3.37.1                               "docker-entrypoint.s…"   2 minutes ago       Up 2 minutes        2368/tcp                                   ghost_ghost-blog_2
2df6c27c1fe3        ghost:3.36                             "docker-entrypoint.s…"   9 days ago          Up 7 days           2368/tcp                                   ghost_ghost-blog_1
89a5a7fdcfa4        jrcs/letsencrypt-nginx-proxy-companion   "/bin/bash /app/entr…"   9 days ago          Up 7 days                                                      letsencrypt-helper
90b72e217516        jwilder/nginx-proxy                      "/app/docker-entrypo…"   9 days ago          Up 7 days           0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp   reverse-proxy

请注意,旧容器的名称是 ghost_ghost-blog_1. 检查您的域名,您会发现它仍然可以在 blog.domain.com 上访问。 如果你刷新幽灵 admin 扩展后位于 blog.domain.com/ghost 的面板,它会一直尝试加载自身,直到您删除旧容器:

[email protected]:~/ghost$ docker rm -f ghost_ghost-blog_1

但是对于 Ghost 博客本身来说,根本没有停机时间! 因此,通过这种方式,您可以在更新 Ghost 博客时确保零停机时间。

最后,将配置缩小到其原始设置:

[email protected]:~/ghost$ docker-compose up -d --scale ghost-blog=1 --no-recreate
Starting ghost_ghost-blog_2 ... done

如前所述,移除旧容器后,更改会反映在各自的 Web 应用程序中,但由于应用程序设计不同,它们的行为显然有所不同。

以下是一些观察:

在 WordPress 上:确保您添加了define(‘AUTOMATIC_UPDATER_DISABLED’, true); 作为位于 /var/www/html 的文件 wp-config.php 的底线,并挂载 /var/www/html/wp-content 而不是 /var/www/html 作为卷。 查看 这里 详情。 在第 3 步之后,WordPress admin 面板将显示您的 WordPress 是最新的,并会要求您继续并更新您的数据库。 更新很快发生,WordPress 网站没有任何停机时间,就是这样!

在 Rocket.Chat 上: 大约需要 15-20 秒 Admin>Info 页面显示您正在运行最新版本,甚至在您执行第 3 步之前。不再需要停机!

在 Nextcloud 上:在第 2 步之后,Nextcloud 会切换到维护模式几秒钟,然后再次加载您的文件。 在下面 Administration > Overview > Security & setup warnings,您可能会收到警告,例如“您的 Web 服务器未正确设置为解析“./well-known/carddav”。这是因为您的旧容器仍在运行。使用第 3 步删除它后,此警告将不再存在。确保在访问 Nextcloud URL 之前给它一些时间,因为它可能会显示 502 bad gateway 错误,直到你的 Nginx 容器看到基于最新版本的新扩展的 Nextcloud 容器。

奖金提示

以下是遵循此方法时要记住的一些提示和事项。

提示 1

为了使不同应用程序的停机时间最小化为零,请确保为新扩展的最新容器提供足够的时间,以便 Nginx 容器最终可以在您在步骤 2 中删除旧容器之前确认它们。

在进行上述第 2 步之前,建议在执行第 1 步后观察应用程序在浏览器上的行为(登录后的页面刷新或私有、无缓存浏览器窗口上的新页面访问)。

由于每个应用程序的设计都不同,因此在您关闭旧容器之前,此措施将非常有用。

提示 2

虽然这可能是将容器更新到它们运行的​​应用程序的最新版本的一种非常有用的方法,但请注意,您可以使用相同的方法进行任何配置更改或修改环境设置,而不会遇到停机问题。

如果您必须对问题进行故障排除或在活动容器中执行您认为必要的更改,但又不想在这样做时将其关闭,这可能至关重要。 进行更改并确保问题已解决后,您可以轻松关闭旧版本。

当我们发现在我们的一个实时容器中没有启用日志轮换时,我们自己就遇到了这个问题。 我们进行了必要的更改以启用它,同时在使用此方法时避免了任何停机时间。

提示 3

如果您在 YML 文件中使用 container_name,该方法将无法按预期工作,因为它涉及创建新的容器名称。

您可以通过将容器命名任务留给 Docker Compose(根据其命名约定自动完成)来避免这种冲突。 Docker Compose 将其容器命名为 directory-name_service-name_1. 每次更新容器时,最后的数字都会增加(直到您出于某种原因使用 docker-compose down)。

如果您已经通过在 docker compose 文件中使用自定义命名容器来使用服务,要使用此方法,只需注释掉包含以下内容的行(带有 # 前缀)container_name 在服务定义中。

在使用本教程中所述的步骤 1 为上述内容创建新容器后,对于步骤 2,旧容器(未停止以避免停机)名称将与之前使用指定的名称相同 container_name(也可以用`检查docker ps` 在移除旧容器之前)。

你学到新东西了吗?

我知道这篇文章有点长,但我希望它能帮助我们的 DevOps 社区管理运行 dockerized Web 应用程序的生产服务器的停机问题。 停机时间确实对个人和商业层面都有巨大的影响,这就是为什么涵盖这个主题是绝对必要的。

请加入讨论并在下面的评论部分分享您的想法、反馈或建议。