为了运行 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 似乎建议构建一个新的镜像而不是修改一个现有的容器,所以我认为像我这样做的重建的一般方式是正确的,但是实现中的一些细节必须改进。
带有视觉解释的视频(从 2022 年开始)
https://i.stack.imgur.com/PgYH2.png
由于我得到了很多积极的反馈to my previously, first visual explanation,我决定为这个问答制作另一个视频,因为有些东西可以在图形视频中更好地可视化。它可视化并使用我在过去几年在多个系统(以及 K8s)上使用 Docker 获得的知识和经验来更新这个答案。
虽然这个问题是在 ASP.NET Core 的上下文中提出的,但它与这个框架并没有真正的关系。问题是缺乏对 Docker 概念的基本理解,因此几乎每个应用程序和框架都会发生这种情况。出于这个原因,我在这里使用了一个简单的 Nginx 网络服务器,因为我想你们中的许多人都熟悉网络服务器,但并不是每个人都知道像 ASP.NET Core 这样的特定框架是如何工作的。
根本问题是了解容器与图像的区别以及它们在生命周期中的不同之处,这是本视频的基本主题。
文字答案(最初来自 2016 年)
经过一番研究和测试,我发现我对 Docker 容器的生命周期存在一些误解。当镜像同时重建时,简单地重新启动容器不会使 Docker 使用新镜像。相反,Docker 仅在创建容器之前获取图像。所以容器运行后的状态是持久的。
为什么需要删除
因此,重建和重新启动是不够的。我认为容器就像服务一样工作:停止服务,进行更改,重新启动它,它们就会应用。那是我最大的错误。
由于容器是永久性的,因此您必须先使用 docker rm <ContainerName>
删除它们。移除容器后,不能简单地通过 docker start
启动它。这必须使用 docker run
来完成,它本身使用最新的图像来创建新的容器实例。
容器应尽可能独立
有了这些知识,就可以理解为什么将数据存储在容器中是 qualified as bad practice 而 Docker 建议改为 data volumes/mounting host directorys:由于必须销毁容器才能更新应用程序,因此其中存储的数据也会丢失。这会导致额外的工作来关闭服务、备份数据等。
因此,将这些数据完全从容器中排除是一个明智的解决方案:当数据安全地存储在主机上并且容器只保存应用程序本身时,我们不必担心我们的数据。
为什么 -rf 可能无法真正帮助您
docker run
命令有一个名为 -rf
的 Clean up 开关。它将停止永久保留 docker 容器的行为。使用 -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 build
、docker run
和其他简单的 docker 命令是初学者理解基本概念的好方法,但当您已经熟悉 Docker 并想要提高工作效率时,它会变得很烦人。更好的方法是使用 Docker-Compose。虽然它是为多容器环境而设计的,但在使用单个容器独立使用时,它也会给您带来好处。 Altough 多容器环境并不少见。几乎每个应用程序都至少有一个应用程序服务器和一些数据库。有些甚至更喜欢缓存服务器、cron 容器或其他东西。
version: "2.4"
services:
my-container:
build: .
ports:
- "5000:5000"
现在您只需使用 docker-compose up --build
,compose 将处理我手动执行的所有步骤。我更喜欢这个脚本而不是带有普通 docker 命令的脚本,这是我从 2016 年开始添加的答案。它仍然有效,但更复杂,它可以处理某些情况,不如 docker-compose 好。例如,compose 检查是否所有内容都是最新的,并且只重建那些因更改而需要重建的东西。
特别是当您使用多个容器时,compose 提供了更多好处。例如,链接需要手动创建/维护网络的容器。您还可以指定依赖关系,以便在应用程序服务器之前启动一个数据库容器,该应用程序服务器在启动时依赖于数据库。
在过去使用 Docker-Compose 1.x 时,我注意到了一些问题,尤其是在缓存方面。这会导致容器没有被更新,即使发生了一些变化。我已经测试了 compose 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 或 compose 或需求进行更改时,请使用 docker-compose up --build
重新运行它。以便图像得到重建和刷新
/opt/mysql/data:/var/lib/mysql
的卷,之后数据库是否会为空?
--build
似乎没有任何缺点。 docker 重新复制它可能认为不需要复制的文件的速度只需要几毫秒,并且它节省了大量的 WTF 时刻。
您可以通过运行 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;
}
--force-recreate
选项是否与您在此处描述的相似?如果是这样,那么使用这个解决方案是否值得(对不起,如果这个问题很愚蠢,但我是一个 docker noob ^^)docker-compose
比普通的docker
命令更智能。我经常使用docker-compose
并且更改检测效果很好,所以我很少使用--force-recreate
。当您构建自定义图像(撰写文件中的build
指令)而不是使用来自 Docker 中心的图像时,只有docker-compose up --build
很重要。