在文件更改时重新生成 Docker 容器

为了运行 ASP.NET Core 应用程序,我生成了一个 dockerfile,用于构建应用程序并在容器中复制源代码,Git 使用 Jenkins 获取源代码。因此,在我的工作区中,我在 dockerfile 中执行以下操作:

WORKDIR /app
COPY src src

虽然 Jenkins 使用 Git 正确地更新了我主机上的文件,但 Docker 并没有将其应用到我的映像上。

我的基本建筑脚本:

#!/bin/bash
imageName=xx:my-image
containerName=my-container


docker build -t $imageName -f Dockerfile  .


containerRunning=$(docker inspect --format="{{ .State.Running }}" $containerName 2> /dev/null)


if [ "$containerRunning" == "true" ]; then
docker stop $containerName
docker start $containerName
else
docker run -d -p 5000:5000 --name $containerName $imageName
fi

我为 docker run尝试了不同的东西,比如 --rm--no-cache参数,还停止/删除了正在构建的容器 之前。我不知道我哪里做错了。看起来 docker 正在正确地更新图像,因为调用 COPY src src将导致一个图层 id 而没有缓存调用:

Step 6 : COPY src src
---> 382ef210d8fd

更新容器的推荐方法是什么?

我的典型场景是: 应用程序在 Docker 容器中的服务器上运行。现在应用程序的一部分已经更新,例如通过修改一个文件。现在容器应该运行新版本。Docker 似乎建议构建一个新的映像,而不是修改现有的容器,所以我认为像我这样重建的一般方法是正确的,但是实现中的一些细节需要改进。

195731 次浏览

视像解说短片(由2022年起)

Visual video explanation container vs images

因为我得到了很多正面的反馈 我之前的第一个视觉解释,我决定为这个问题创建另一个视频和答案,因为有一些事情可以在图形视频更好地可视化。它可视化并更新这个答案的知识和经验,我在过去几年使用 Docker 在多个系统(也 K8)。

虽然这个问题是在 ASP.NET Core 的上下文中提出的,但它与这个框架并没有真正的关系。问题在于缺乏对 Docker 概念的基本理解,因此几乎每个应用程序和框架都会出现这种情况。出于这个原因,我在这里使用了一个简单的 Nginx web 服务器,因为我认为你们中的很多人都熟悉 web 服务器,但是并不是每个人都知道像 ASP.NET Core 这样的特定框架是如何工作的。

潜在的问题是理解容器与图像的区别,以及它们在生命周期中的不同之处,这是本视频的基本主题。

原文回答(2016年)

经过一些研究和测试,我发现我对 Docker 容器的寿命存在一些误解。仅仅重新启动一个容器并不能使 Docker 在同时重新构建映像时使用一个新映像。相反,Docker 只获取创建容器的 之前映像。因此,运行容器后的状态是持久的。

为什么需要移除

因此,重建和重新启动是不够的。我认为容器像服务一样工作: 停止服务,进行更改,重新启动它,它们就会应用。那是我最大的错误。

因为容器是永久性的,所以必须首先使用 docker rm <ContainerName>删除它们。删除容器后,不能简单地通过 docker start启动它。这必须使用 docker run来完成,它本身使用最新的映像来创建一个新的容器实例。

容器应尽可能独立

有了这些知识,就可以理解为什么在容器中存储数据是 被认为是不良行为,Docker 推荐使用 数据卷/挂载主机目录: 因为必须销毁容器来更新应用程序,所以容器中存储的数据也会丢失。这会导致关闭服务、备份数据等等的额外工作。

因此,从容器中完全排除这些数据是一个明智的解决方案: 当数据安全地存储在主机上且容器只保存应用程序本身时,我们不必担心我们的数据。

为什么 -rf不能真正帮助你

docker run命令有一个名为 -rf收拾干净开关。它将永久停止保留码头容器的行为。使用 -rf,Docker 将在容器退出后销毁它。但是这个开关有一个问题: Docker 还删除了没有与容器关联的名称的卷,这可能会杀死您的数据

虽然 -rf开关是一个很好的选择,可以节省开发过程中的工作,以便进行快速测试,但它不太适合生产。特别是因为缺少在后台运行容器的选项,而后台运行大多数情况下都是必需的。

如何移除容器

我们可以通过简单地移除容器来绕过这些限制:

docker rm --force <ContainerName>

在运行的容器上使用 SIGKILL 的 --force(或 -f)开关。相反,你也可以在以下情况之前停止容器:

docker stop <ContainerName>
docker rm <ContainerName>

两者是平等的。docker stop也在使用 SIGTERM。但是使用 --force开关会缩短脚本,特别是在使用 CI 服务器时: 如果容器没有运行,docker stop会抛出一个错误。这将导致 Jenkins 和许多其他 CI 服务器错误地认为构建失败。为了解决这个问题,您必须首先检查容器是否像我在问题中所做的那样运行(参见 containerRunning变量)。

还有更好的办法(2016年增补)

虽然像 docker builddocker run和其他普通的 Docker 命令对于初学者来说是理解基本概念的好方法,但是当你已经熟悉 Docker 并希望获得高效时,它就变得令人厌烦了。更好的方法是使用 Docker-Compose。虽然它是为多容器环境设计的,但是在单个容器独立使用时,它也给您带来了好处。尽管多容器环境并不少见。几乎每个应用程序都至少有一个应用服务器和一些数据库。有些甚至更像缓存服务器、 cron 容器或其他东西。

version: "2.4"
services:
my-container:
build: .
ports:
- "5000:5000"

现在您可以只使用 docker-compose up --build和撰写将照顾所有的步骤,我手动完成。我更喜欢使用这个脚本而不是使用普通的 docker 命令,这是我在2016年作为答案添加的。它仍然可以工作,但是更加复杂,并且它可以处理某些情况,而不像 docker-compose 那样好。例如,编写检查是否所有内容都是 up2date 并且只重新构建这些内容,因为更改而需要重新构建这些内容。

特别是当您使用多个容器时,撰写提供了更多的好处。例如,链接需要手动创建/维护网络的容器。您还可以指定依赖项,以便在应用程序服务器之前启动数据库容器,这取决于启动时的 DB。

在过去使用 Docker-Compose 1.x 时,我注意到了一些问题,尤其是缓存方面的问题。这会导致容器不被更新,即使某些内容发生了更改。我测试撰写 v2已经有一段时间了,没有再看到任何这些问题,所以现在看起来已经修复了。

重建 Docker 容器的完整脚本(原始答案 vom 2016)

根据这个新知识,我用以下方法修改了我的剧本:

#!/bin/bash
imageName=xx:my-image
containerName=my-container


docker build -t $imageName -f Dockerfile  .


echo Delete old container...
docker rm -f $containerName


echo Run new container...
docker run -d -p 5000:5000 --name $containerName $imageName

这种方法非常有效:)

每当在 dockerfile 中进行更改或撰写或需求时,使用 docker-compose up --build重新运行它。这样就可以重建和刷新图像

通过运行 docker-compose up --build <service name>,您可以为特定的服务运行 build,其中的服务名称必须与您在 docker-compose 文件中如何调用它相匹配。

例子 让我们假设您的 docker-compose 文件包含许多服务(。Net 应用程序-数据库-让我们加密... 等) ,你只想更新。在文件中命名为 application的 net 应用程序。 然后可以简单地运行 docker-compose up --build application

额外的参数 如果您想为命令添加额外的参数(如 -d)以便在后台运行,那么参数必须位于服务名称之前: docker-compose up --build -d application

您可以仅从副本强制进行重新生成,而不必进行完全重新生成。

加一条类似于

RUN mkdir -p /BUILD_TOKEN/f7e0188ea2c8466ebf77bf37eb6ab1c1
COPY src src

Mkdir 调用只是有一行 docker 必须执行的代码,其中包含每次需要部分重建时要更改的令牌。

现在,让您的构建脚本在需要强制执行副本时替换 uuid

在飞镖里我这样做:


if (parsed['clone'] as bool == true) {
final uuid = const Uuid().v4().replaceAll('-', '');


replace(dockerfilePath, RegExp('RUN mkdir -p /BUILD_TOKEN/.*'),
'RUN mkdir -p /BUILD_TOKEN/$uuid');
}

然后我运行我的构建工具:

build.dart --clone

这是我的完整飞镖脚本,但它有一些无关的地方:

#! /usr/bin/env dcli


import 'dart:io';


import 'package:dcli/dcli.dart';
import 'package:mongo_dart/mongo_dart.dart';
import 'package:unpubd/src/version/version.g.dart';


/// build and publish the unpubd docker container.
void main(List<String> args) {
final parser = ArgParser()
..addFlag('clean',
abbr: 'c', help: 'Force a full rebuild of the docker container')
..addFlag('clone', abbr: 'l', help: 'Force reclone of the git repo.');


ArgResults parsed;
try {
parsed = parser.parse(args);
} on FormatException catch (e) {
print(e);
print(parser.usage);
exit(1);
}
final dockerfilePath =
join(DartProject.self.pathToProjectRoot, 'resources', 'Dockerfile');


'dcli pack'.run;


print(blue('Building unpubd $packageVersion'));


final tag = 'noojee/unpubd:$packageVersion';
const latest = 'noojee/unpubd:latest';


var clean = '';
if (parsed['clean'] as bool == true) {
clean = ' --no-cache';
}


if (parsed['clone'] as bool == true) {
final uuid = const Uuid().v4().replaceAll('-', '');
replace(dockerfilePath, RegExp('RUN mkdir -p /BUILD_TOKEN/.*'),
'RUN mkdir -p /BUILD_TOKEN/$uuid');
}


'docker  build $clean -t $tag -t $latest -f $dockerfilePath .'.run;


'docker push noojee/unpubd:$packageVersion'.run;
'docker push $tag'.run;
'docker push $latest'.run;
}