Matthias Haeussler / Eva Panadero @ Spring I/O 2025:Spring Boot 与容器的新变化




内容概要

Matthias Haeussler 和 Eva Panadero 探讨了 Spring Boot 与容器的集成,内容涵盖了简化 Java 应用在 Docker 和 Kubernetes 中构建、测试和运行的基本工具与功能。本次演讲不仅探讨了已有的集成点,还介绍了最新的更新和新功能,例如 Buildpack、原生镜像 (native image)、Testcontainer、Docker Compose 集成以及在 Kubernetes 中的安全部署,同时还展望了 Docker Model Runner 和 Spring AI 的集成。

目录

  • 介绍与背景

  • 容器中的 Spring Boot 应用

  • 构建容器镜像:Dockerfile 与 Buildpack

  • 使用 GraalVM 构建原生镜像 (Native Image)

  • 使用 Docker Compose 与 Testcontainer 进行测试

  • Spring AI 与 Docker Model Runner

  • 在 Kubernetes 中安全部署

  • 使用开发容器 (Dev Container) 构建开发环境

  • 总结与问答

介绍与背景

Matthias Haeussler: 感谢大家的到来。我们先慢慢开始,因为还有些人正在入场。大家好,欢迎来到我们的分享,主题是“Spring Boot 与容器有哪些新动态?”。我们给“新”加上了括号,因为内容不完全是最新的。

这位是我的同事 Eva,我是 Matias。我们都来自一家叫 Novatech 的公司,这家公司即将与 CGI 合并。所以严格来说,这是我们作为 Novatech 员工的最后一次演讲。

我的专业领域是云原生软件工程,既涉及软件开发,也涉及平台方面,这也是我们这次演讲主题的背景。

显然,容器 (container) 和 Spring Boot 都不是全新的技术。我试着整理了这张时间线,上面是容器技术,下面是 Spring Boot。这张图并非严格按时间线性排列,因为两者的历史都可以追溯到很久以前,比如容器技术可以追溯到 1979 年的 chroot,而 Java 则始于 1995 年。

大约在 2013 到 2014 年,随着 Docker、Kubernetes 和 Spring Boot 的出现,事情变得有趣起来。此后,在这两个领域涌现了大量技术,如 Compose、Testcontainer、Buildpack、Spring Cloud、Spring Native 等。它们各自都能独立发展得很好,但彼此之间也相互关联。

这就是今天演讲的主题。我们想展示有哪些实用的集成方式,特别是 Spring Boot 提供了哪些便利,让整合过程更简单,以及除了将应用打包进容器之外,还有哪些其他的可能性。

容器中的 Spring Boot 应用

当人们想到 Spring Boot 和容器时,最直接的想法就是将 Spring Boot 应用打包进一个容器里,通常的结构是底层一个基础层,中间是运行时,顶层是应用程序。

容器提供了非常好的打包标准化,使得应用环境高度一致。你可以轻松地将其移植到不同平台,进行伸缩和重复部署。这些是核心优势,但容器的应用远不止于打包。

你也可以在实际的开发步骤中使用容器,比如为多个开发者提供一致的开发环境,以避免环境的“野蛮生长”。

你还可以在构建步骤中应用标准化机制,将容器用作构建工具,当然也包括在流水线 (pipeline) 中。

接下来是测试,这可能是我们今天最重要的部分之一。因为据我所知,这可能是容器技术在软件开发中最广泛的应用了。通过容器,可以非常轻松地启动一个接近真实世界的后端环境。我们会展示几种实现方法。

当然,还有应用的运行,尤其是在分布式和多语言环境中,以及在企业级应用中实现规模化。实际上,相关的集成点非常多,我想大家可能都听说过大部分。希望今天的内容里还有一些你们不知道的新东西,能让大家的工作更轻松。

构建容器镜像:Dockerfile 与 Buildpack

在容器中构建应用时,最常见的方式是使用所谓的 Dockerfile。Dockerfile 是一系列顺序执行的命令,最终生成一个容器镜像 (container image) 的不同层 (layer)。

这种方式让你能够精细地构建和调整镜像。通常,底层是基础层,顶层是应用层,中间是依赖层。根据迭代重建的特性,顶层的内容应该比底层的内容变更得更频繁。

这个原则同样适用于 Java 世界,开发者通常将 JAR 文件放在顶层。JAR 文件中包含了类文件、资源文件和依赖 JAR 包。你的类文件每次构建都可能变化,而依赖则未必。

使用 Dockerfile 的问题在于,你有很多选择。你可以创建简单的,但优化不足;也可以创建优化的,但过程相当复杂。

其中一种优化方法是将 JAR 文件拆分成多个独立的层,包括依赖层、Spring Boot 加载器层、快照依赖层和实际的应用层。这样,在每次迭代重建时,如果只有应用部分发生变化,其他所有层都可以利用缓存,只需要重新构建变化的部分。

你还可以优化 Java 运行时本身。有一些工具叫做 JDEPs 和 JLink。JDEPs 可以分析你的应用,找出运行它实际需要的 JRE 模块。然后 JLink 可以基于这些信息构建一个精简的自定义 JRE,只包含必需的部分。

这样做一方面可以减小镜像体积,另一方面也减少了攻击面,因为容器里没有的东西自然不会被利用。

如果你应用了所有这些优化,你的 Dockerfile 可能会变得像一门独立的编程语言,需要专门去维护和管理。

从优化的 Dockerfile 中我们可以总结出几点:尽量使用一个简单且一致的基础镜像,即使是跨语言项目也一样;使用 JLink 和 JDEPs 优化运行时;并尽可能好地组织你的应用层。

Spring Boot 生态提供了一个叫做 Buildpack 的工具。你可能听说过,它起源于 Heroku 和 Cloud Foundry 这类平台即服务 (PaaS) 技术。当时的想法是,你将代码推送到平台,平台会自动检测、构建,并将你的应用导出为一个可运行的实体——现在我们称之为容器,并负责整个生命周期。

这个过程也可以在 PaaS 之外独立进行。你可以使用 pack 命令,告诉它使用哪个构建器镜像 (builder image),然后你的源代码会被注入,生命周期被处理,最终得到一个结果镜像。

这种方式最大的优点是,真的只需要一行代码。这基本上就是你在 pom 文件或 gradle 文件所在目录下执行的 pack 命令,然后它会为你完成所有事情。

如果你想实现像 JLink 那样的优化,只需要加几个命令行开关,但仍然是一行代码,就能获得通常只有在非常复杂的 Dockerfile 中才能实现的所有好处。pack 是多语言支持的,你可以在不同语言项目中使用。但如果你正好在使用 Spring Boot,还可以将其作为 Maven 或 Gradle 插件使用,这样就不需要 pack 命令行工具,可以轻松集成到你现有的工作流中。

使用 GraalVM 构建原生镜像 (Native Image)

Eva Panadero: 首先,我想解释一下在这次分享中我们如何使用“镜像 (image)”这个词。我们大多数时候指的是 Docker 镜像,但在这个领域还有另一个重要的角色,那就是原生镜像 (native image),两者之间有很大的区别。

一个 Docker 镜像包含了你的应用运行所需的一切:操作系统、运行时、应用库等等,所有你需要的东西都在这个容器镜像里。

但什么是原生镜像呢?它是完全不同的东西。它是一个通过预编译 (ahead-of-time) 生成的单一二进制文件。它更小、更快,并且不需要运行时。

有趣的地方在于,你不需要二选一。你可以将两者结合起来,把一个原生二进制文件打包进 Docker 镜像里。这样你就能同时享受到两者的好处:既有可移植性,又有原生镜像带来的高性能。

接下来我将演示一下我所解释的内容。这是我们的 Graal 原生镜像项目,一个非常简单的项目,只有一个应用和一个用于从 Spring 返回 "hello" 的控制器。

我已经提前编译好了所有的镜像,因为正如 Matias 所说,我们没有时间在现场进行编译。

要构建传统的 JAR 文件,我们只需运行 Maven 的 clean package 命令,就可以在 target 目录下找到这个 JAR 文件。稍后我会逐一运行它。

我们的应用启动耗时 1.048 秒。需要注意的是,这是一个非常小的应用。

现在我们来看看原生镜像的情况。你需要先在本地机器上安装 GraalVM,并添加 Spring Boot 的 GraalVM 依赖。运行一个命令后,你就会得到这个二进制文件。让我们用同样的方式运行它。

可以看到,它几乎是瞬间启动的。这种性能在需要应用伸缩的场景中非常重要,比如在 Kubernetes 集群中扩展 pod。这非常有益。

现在,我来展示我们预构建的两个 Docker 镜像。第一个是针对 JAR 文件的,大小为 261 MB。另一个是原生二进制文件的,大小为 114 MB。这里还有一个原生镜像,是 Matias 提到的用 pack 命令构建的,它的体积更小。

我先运行包含 JAR 文件的 Docker 镜像。它的启动时间和第一个差不多,大约是 1.05 秒。

现在我们来试试最后一个镜像,即包含原生镜像的 Docker Run。如你所见,它启动得非常快,我们没有损失原生镜像的任何性能。

结论是,结合使用可以获得更好的性能,同时保留 Docker 镜像和原生镜像的所有优点。

Matthias Haeussler: 这部分属于演讲的“构建”环节,但如你所见,它已经自然过渡到了“运行”环节,并同时演示了传统 JAR 和原生镜像两种方式。重要的一点是,将应用放入容器中你不会损失任何东西。这种开销几乎为零,并且两种方法都适用。

使用 Docker Compose 与 Testcontainer 进行测试

这部分,至少根据我们的经验,是容器技术应用最广泛的领域。即使你不使用任何框架,也可以直接说:“我需要一个数据库、一个消息系统,或者其他任何服务,都放在容器里。”

通过一个简单的 docker run 命令,我就可以快速、轻松地启动所需服务,而无需在我的系统上安装任何东西。对于一个 Spring 应用来说,我只需要提供连接属性,应用就能与这些服务通信。

然而,有些工具虽然底层做的是同样的事情,但能为你提供一些额外的优势,特别是在 Docker Compose 和 Testcontainer 方面。

Eva Panadero: 到目前为止,我们已经看到了用不同方式运行我们的应用,但当我们的应用依赖于其他服务时,比如数据库,这种情况对我们来说非常普遍,又该怎么办呢?

让我先介绍一下这个项目。在 Docker 项目中,我们同样有一个非常简单的应用,其中包含一个控制器,它有一些用于处理 GET、POST 和 DELETE 请求的端点。然后,我们有一个使用 Spring Data JPA 连接数据库的 ItemRepository

我们在这里所做的,只是在 application.properties 文件中配置了所有需要的信息,比如 URL、驱动、用户名和密码等。然后,我们还有一个 Docker Compose 文件,里面定义了一个非常简单的 PostgreSQL 数据库镜像,使用了相同的用户和密码。

第一种场景是传统方式。我们首先运行 docker-compose up -d 来启动数据库,然后运行我们的 Spring Boot 应用 spring-boot:run

现在应用正在运行。我们可以发送一些请求。如果我们发送一个 GET 请求,结果是空的。但我们可以 POST 一些数据,然后再 GET 一次,就能看到数据了。我们也可以删除它们。这里没什么新鲜的,这是我们过去最常用的工作方式。

现在让我们来看第二种,更“神奇”的场景。Matias,如果我忘了在 application.properties 文件里配置这些信息,你觉得会发生什么?

Matthias Haeussler: 既然我们已经练习了好几次,我知道会发生什么。但如果你在不久前问我,我可能会认为应用会启动失败,因为它找不到数据库连接。

Eva Panadero: 让我们假设正确的答案是:应用会崩溃,甚至无法启动。但在这种情况下,应用却正常运行了。你可能会想:“这是什么魔法?”

让我们直接跳到第三种场景,因为我暂时还不想揭晓谜底。第三种场景是进入“上帝模式”。我们既没有配置,也没有运行 docker-compose up。我们没有任何 Docker Compose 实例,application.properties 里也没有任何配置。如果我们再次运行应用,可以看到,应用不仅正常运行,而且功能完好。

我可以做个测试,获取所有项目,结果是空的。然后我创建一个,再获取一次,就能看到了。

这是最简单的方式。很遗憾,我得揭晓这个魔法了。所有的奥秘都藏在我们项目的一个依赖里。如果我注释掉这个依赖,一切都会像我们最初预期的那样崩溃。

有了这个依赖,一切都自动化了。它会在 compose.yml 文件中查找所有需要的元数据来配置一切。如果我们没有启动 Docker Compose,它会自动启动,并等待服务健康后才启动我们的 Spring Boot 应用。

Matthias Haeussler: 这是一种利用 Spring Boot “魔法”的方式。如果你不想操心 application.properties 里的配置,这无疑很有帮助,应用会直接从 Docker Compose 文件中读取配置。

当然,这也意味着如果你更新了 Docker Compose 文件并做了改动,这些改动会反映到你的应用中。这一点人们容易忘记。如果 Compose 文件和属性文件之间出现不匹配,你的应用就无法正常工作。所以这绝对是一个可以避免的陷阱。

另一种实现方式是使用 Testcontainer 技术。我们项目的底层应用基本是一样的。

在底层,Docker Compose 依赖项会检查是否存在一个名为 docker-compose.ymlcompose.yaml 的文件,并从中读取属性,比如 PostgreSQL 数据库、用户和密码,然后自动将其注入到应用中,无需你手动操作。

使用 Testcontainer 时,也会发生类似的事情。这项技术已经存在一段时间了,而且非常简单易用。在 Spring Boot 3.1 之前,你通常需要这样做:在测试类上加上 @Testcontainers 注解,然后用这个注解来启动容器。

那时我需要做的是自动配置属性,这样当 Testcontainer 启动时,它会带有一些默认设置,这些设置会被映射到 Spring Boot 的属性中。

在 3.1 的新版本中,有了一些变化。现在有了一个 TestcontainersConfiguration 类。在这里,最关键的是 @ServiceConnection 这个注解,它会自动获取服务的连接详情,并将其放入 Spring Boot 的环境中。当然,你也可以根据需要覆盖它们。

如果我运行 mvn test,你可以在日志中看到它正在连接 Docker,启动所有需要的东西,并运行测试用例。你可以混合使用新旧版本,完全没有问题。测试完成后,它会自动清理并退出。

还有一个新的 Maven goal 叫做 test run。你可以看到,我的测试文件夹里也有一个带 main 方法的类。当这个类被调用时,它会调用应用的实际主类,但会把 Testcontainer 的配置传递给它。

所以,如果我运行 maven spring-boot:test-run 而不是 run,它仍然会运行主应用,但会包含 Testcontainer 的配置。这也意味着你不需要指定任何东西。即使我之前没有任何容器,应用现在也能正常运行。

如果我查看 docker ps,可以看到容器已经在那儿了,并且可以正常工作。我甚至可以与我的应用进行交互。我还写了一个自定义的控制器,用来检查那些被自动注入的连接详情。我在这里使用了构造函数注入,然后遍历了所有的连接细节。

如果我再次运行它,我可以看到有三个不同的连接详情:一个用于 Liquibase,一个用于 Flyway,还有一个来自 PostgreSQL 容器。在这里你可以看到所有我从未设置过的值。如果我查看 application.properties 文件,会发现所有配置都被注释掉了。

这和 Eva 之前展示的 Docker Compose 的底层机制非常相似。所以,如果你不需要手动指定这些名称和属性,就不太会出错。

你有不同的选择。你仍然可以手动完成所有事情,但明显的缺点是你必须同时配置容器和应用的容器连接信息。使用 Docker Compose,你只需要在 Compose 文件中定义,它就像一个外部数据源,Spring 应用会自动读取并加载连接详情。

另一方面,Testcontainer 的工作方式类似,但你不是在外部文件中指定测试容器的属性,而是在代码中直接定义。两者各有优劣,但我确信,选择其中任何一种,而不是全部手动操作,都肯定能让你受益。

Spring AI 与 Docker Model Runner

AI 这个话题是我们提交了演讲之后才加进来的。如今,不做一场关于 AI 的演讲似乎都说不过去。

这个概念可以扩展到将 Testcontainer 与本地大语言模型 (LLM) 结合使用。你可能听说过 Ollama。最近,Docker 也推出了一个叫做 Docker Model Runner 的东西,可以让你在本地运行大语言模型。你可以像我刚才展示的那样,用同样的方式通过 Testcontainer 来使用它们。

我有两个模型:Gemma 和另一个小模型。理论上,我可以通过 Docker API 来管理它们,就像管理容器一样。同样,我也可以从 Spring 的上下文中访问它们。

当然,这个应用的 pom.xml 文件里有 Spring AI 的依赖,也有 Testcontainer 的相关配置。如果我们查看测试类,你会发现我的测试配置中,我通过半手动的方式注入了属性,指定了 API 密钥和要使用的模型。

访问 Docker 引擎的端点 (endpoint) 是从 Docker Model Runner 容器中获取的。我有两个模型,Gemma 3 和另一个。另一个在我的资源文件中被指定为默认模型。

如果我运行 maven spring-boot:test-run,它自然会加载配置。应用启动后会暴露一个非常简单的端点,基本上就是一个聊天端点。

如果我现在说:“我想和你聊天”,然后通过一个 HTTP 请求问:“你叫什么名字?”,它很有可能会回复说:“我叫 Gemma。” 这说明它从 Testcontainer 的属性中获取了配置,并覆盖了标准属性文件中的配置。

你也可以将我们刚刚展示的内容扩展到本地大语言模型上。我们刚才和 Docker 的 Oleg 聊过,他说现在 Docker Compose 也支持这个功能了,所以整个故事在这里就完整了。

在 Kubernetes 中安全部署

Eva Panadero: 我不是 Kubernetes 专家,但没关系,因为我们有运维团队和我们最好的朋友——Spring Boot 来帮助我们。

几乎不需要任何配置,Spring Boot 就为我们内置了对存活探针 (liveness probe)就绪探针 (readiness probe) 的支持。当然,Actuator 让这一切魔法都变得可见。

这是我们用于部署的 YAML 文件。在这个文件中,我们配置了 Spring Boot 默认提供的端点,并且创建了两个自定义的健康指示器:一个用于存活探针,一个用于就绪探针。

Matthias Haeussler: 对于不熟悉 Kubernetes 的朋友,这两个探针实际上与 Spring Boot 本身没有直接关系。这只是 Kubernetes 提供的一个机制,让应用可以告诉 Kubernetes:“我现在准备好接收连接了”(这是就绪探针),或者“我还活着并且健康”(这是存活探针)。

如果就绪探针失败,Kubernetes 中的这个组件将不会接收任何流量。如果存活探针失败,该组件将被 Kubernetes 终止。

Eva Panadero: 我们创建了几个自定义的健康指示器,来强制控制器模拟“下线 (down)”和“上线 (up)”这两种状态,分别对应存活探针和就绪探针。

我们的 pod 正在运行,一切正常。这里显示有三次重启,因为我们之前调试过。我们现在要做的是强制让它的状态变为“下线”。

我将模拟就绪状态为“下线”。我调用相应的接口,它返回:“好的,就绪状态已下线。” 我们可以看到,还是同一个 pod,但它显示“未就绪”,正如 Matias 提到的,它不再接受流量了。

如果我现在想强制让就绪状态变为“上线”,由于我们已经无法访问那个端点,我需要耍个小花招,做一个端口转发 (port forwarding),直接跳到那个地方。

现在就绪状态将变为“上线”。我们可以检查 Actuator,看到一切都已恢复正常,pod 也恢复了就绪状态,可以再次接收流量。

最后一点,我要展示的是如果我们强制让存活状态变为“下线”,会发生一些不同的事情。现在 pod 处于“未就绪”状态,因为我们强制让它的存活状态变得不健康。我在 YAML 文件里配置了一个几秒的阈值,当它在几秒内都未就绪时,pod 将会重启以恢复健康。

使用 Spring Boot 配置健康检查、就绪探针和存活探针不仅简单,而且几乎毫不费力。你不需要自己实现所有东西,因为大部分都是自动配置的,这有助于我们避免各种错误。

Matthias Haeussler: 当你在 Kubernetes 中运行应用时,我们经常看到一个情况:Kubernetes 实际上对应用内部发生的事情一无所知。我见过人们为了模拟某些状态,在存活探针和就绪探针里写了各种稀奇古怪的东西。当然,如果这些探针发送了错误的信号,Kubernetes 就会做出反应,可能会导致一些非预期的后果。

使用 Spring Boot 的一个明显优势是,你只需要添加 Actuator 依赖,并使用默认的端点,至少就能获得一个检查 HTTP 端口是否开放的就绪检查。这无疑有助于避免不必要的流量进入一个尚未准备好的 pod 或 Spring Boot 应用。

同样需要理解的是,Eva 刚才演示的操作并不是你需要自己去实现的东西,它只是为了这次演示而特意编写的。在实际应用中,如果你有自定义的就绪检查,比如你想检查某个端点或数据库,你会用类似的方式去实现,然后你的 Actuator 会返回相应的结果,告诉 Kubernetes 如何处理这个应用。

使用开发容器 (Dev Container) 构建开发环境

我们已经看到,即使没有直接交互,Spring Boot 应用也能与 Kubernetes 集成。但如果应用与 Kubernetes API 交互,集成会更加无缝。

在开发方面,我想快速介绍一个叫做 Dev Container 的技术。我们有用于测试的 Testcontainer,自然也应该有用于开发的 Dev Container。这项技术也已经存在一段时间了,最初由微软推出。

你需要做的,是在你的代码仓库的根目录或者一个名为 .devcontainer 的文件夹里,放置一个 devcontainer.json 规范文件。在这个文件中,你可以像 Dockerfile 一样,指定开发环境的具体样貌。

在这个例子中,我指定了使用 Java 21 的基础镜像,最新版本的 Maven 和 Gradle,还需要 SDKMAN、Spring Boot CLI、Docker 引擎和 kubectl。

这意味着,每个使用这个代码仓库并且使用支持 Dev Container 的 IDE(比如 IntelliJ 或 VS Code)的开发者,他们的开发环境都会是完全相同的。

你可以精确地锁定 Java 和 Maven 的版本,避免那些因环境细微差异导致的意外问题。在“自定义”部分,这里的例子偏向 VS Code,但你也可以为 IntelliJ 添加类似的部分来定义你的 IDE 配置。

这些设置会自动应用。当然,VS Code 扩展只对 VS Code 有效。如果列表里没有你需要的功能,你仍然可以运行手动命令。比如,Buildpack 没有对应的功能,我就可以通过执行命令来手动安装。

我们今天没有在 Dev Container 里运行演示,因为我之前展示的 Docker Model Runner 部分无法在这样的环境中工作。那是需要你在本地运行的东西,目前只支持 Mac 和 Windows。但对于其他所有情况,Dev Container 都工作得很好。

这是我们的代码仓库。利用 Dev Container 技术的一个简单方法是,登录 GitHub 后,你可以克隆仓库或使用 Code Spaces。如果你选择创建一个 Code Space,它会在云端为你设置好开发环境,并自动读取那个 Dev Container 规范。

所以,如果你想让别人试用你的代码,并确保他们使用的是正确的环境配置,就在仓库里放一个规范文件,一切就都搞定了。第一次运行时会花点时间,因为它需要构建 Code Space。在底层,它会执行我在 Dev Container 规范里定义的所有步骤,完成后,你就拥有了一个你想要的完整开发环境。

总结与问答

Spring 和容器之间有很多不同的集成点。希望今天的内容里有让大家觉得耳目一新的东西。这是代码仓库的地址:


















https://github.com/maeddes/spring-boot-and-containers-talk

























。如果你想自己动手试试,可以去看看。不过,就像这次演讲一样,它仍然是实验性的。

如果你有任何问题,或者觉得我们遗漏了什么关键内容,请随时告诉我们。我们很可能会在未来重复这个演讲,并希望保持内容的更新。

观众 1: 关于 Docker Compose 模块,是否可以像 Testcontainer 那样随机映射数据库端口?

Matthias Haeussler: 可以。在 Docker Compose 部分,你需要在 Compose 文件里指定它,但你确实可以在那里指定一个随机的值,Spring 应用会自动读取并使用它。

观众 2: Testcontainer 是一个很棒的本地运行功能,它在 Docker 引擎上是如何工作的?

Matthias Haeussler: 如今,至少在我们看来,大多数构建引擎都带有某种形式的容器支持。如果你没有,那么大部分优势就无法体现,因为如果你需要容器技术,那么容器就是先决条件。

我不认为我们今天讲的内容里有任何可以绕过容器技术的东西。否则,你就得在别处远程运行这些服务,然后通过网络连接。当然,这样做无法理想地反映你的测试情况,因为会有网络延迟,环境也不够真实。

观众 2: 你们的配置是怎样的?你们是如何做的?是在某个地方运行,还是因为 Docker-in-Docker 存在安全风险,所以企业公司出于安全考虑不想启用它?

Matthias Haeussler: 大多数流水线,在流水线内部,虽然我的话不一定完全准确,但我认为它还是相当安全的。我们也用过 Podman 的方案,而且大部分流水线本身就已经提供了这种支持,所以有现成的、不需要你手动配置的受支持解决方案。


AI 前线

Replit CEO:AI 驱动的软件创造未来

2025-12-23 22:49:31

AI 前线

Victor Rentea 在 Spring I/O 2025 大会上提出的十大 REST API 设计陷阱

2025-12-23 22:49:37

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索