如何为Scala多项目的各子项目分别构建独立Docker镜像
多模块SBT Scala项目子模块独立打包容器化方案
三种成熟落地路径,均不需要打全项目统一包,每个子模块生成的产物可以直接拷贝到JDK基础镜像独立运行。
方案1:sbt-assembly 按子模块单独打可执行fat jar(适配成本最低、最稳妥)
很多人误以为sbt-assembly只能给整个项目打全量fat jar,实际上该插件原生支持多项目构建下的子模块独立打包,只要避免全局配置、给非部署模块配置跳过规则即可,打出来的jar只包含当前子模块的代码+它自身的传递依赖,不会混入其他无关子模块的内容,是生产环境使用最广泛的方案。
- 首先在
project/plugins.sbt中引入插件:addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.5") - 在
Build.scala中按规则配置:根项目和公共依赖模块配置跳过打包,仅给需要独立部署的子模块添加assembly配置,示例:import sbt._ import Keys._ import sbtassembly.AssemblyPlugin.autoImport._ object Build extends Build { // 可部署子模块复用的公共打包配置 lazy val deployableSettings = Seq( assembly / assemblyMergeStrategy := { case PathList("META-INF", _*) => MergeStrategy.discard case _ => MergeStrategy.first }, assembly / assemblyJarName := s"${name.value}-${version.value}.jar" ) // 根项目仅做聚合,跳过所有打包 lazy val root = Project(id = "root", base = file(".")) .aggregate(util, core, serviceA, serviceB) .settings( publish / skip := true, assembly / skip := true ) // 公共依赖模块(被其他模块引用、不需要独立部署),跳过打包 lazy val util = Project(id = "util", base = file("util")) .settings( publish / skip := true, assembly / skip := true ) lazy val core = Project(id = "core", base = file("core")) .dependsOn(util) .settings( publish / skip := true, assembly / skip := true ) // 需要独立部署的子模块A lazy val serviceA = Project(id = "service-a", base = file("service-a")) .dependsOn(core, util) .settings(deployableSettings: _*) .settings( assembly / mainClass := Some("com.yourorg.serviceA.Main") ) // 需要独立部署的子模块B lazy val serviceB = Project(id = "service-b", base = file("service-b")) .dependsOn(core, util) .settings(deployableSettings: _*) .settings( assembly / mainClass := Some("com.yourorg.serviceB.Main") ) } - 打包时指定子模块执行命令即可,比如打serviceA的包:
生成的可执行jar在sbt serviceA/assemblyservice-a/target/scala-<版本号>/目录下,直接拷贝到JDK镜像中,执行java -jar <jar包名>即可运行。
方案2:sbt-native-packager 打分层依赖包(镜像构建效率更高)
如果觉得fat jar合并过程繁琐、镜像层复用率低,可以用这个插件给每个子模块生成分层结构的分发包,不需要做jar合并,打包速度更快,依赖层可以被Docker缓存复用,适合迭代频繁的生产环境。
- 在
project/plugins.sbt中引入插件:addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.16") - 给需要部署的子模块开启Java应用打包能力,公共模块和根项目同样配置跳过:
// 以serviceA为例 lazy val serviceA = Project(id = "service-a", base = file("service-a")) .dependsOn(core, util) .enablePlugins(JavaAppPackaging) .settings( Compile / mainClass := Some("com.yourorg.serviceA.Main") ) - 执行打包命令:
生成的产物在sbt serviceA/stageservice-a/target/universal/stage/目录下,结构为:bin/:自动生成的各平台启动脚本lib/:当前子模块代码+所有传递依赖的jar包conf/:配置文件目录
把整个stage目录拷贝到JDK基础镜像中,直接执行bin/service-a即可启动。
方案3:sbt-pack 打轻量分发包(配置最简单)
如果只需要简单的依赖收集+启动脚本生成,不需要native-packager的多余功能,可以选择sbt-pack,配置更轻量,打包速度更快,该插件不会做jar内容合并,完全不需要配置合并规则,能省下调适merge冲突的时间,适合依赖关系复杂的项目,同样支持子模块独立打包。
- 引入插件后,给需要部署的子模块添加对应配置,执行
sbt serviceA/pack即可生成产物,产物结构和native-packager的stage输出类似,在service-a/target/pack/目录下,拷贝到容器中运行bin目录下的启动脚本即可。
容器化实操注意事项
- 只要在Project定义中通过
dependsOn声明了子模块间的依赖关系,以上三个插件都会自动把依赖模块的代码和传递依赖打进产物,不需要手动处理 - 不要直接执行全局的
sbt assembly/sbt stage/sbt pack命令,必须带上子模块名前缀,避免给不需要部署的模块生成无用产物 - 基础镜像建议选JRE版本而非全量JDK版本,缩小镜像体积,比如用
eclipse-temurin:17-jre-jammy即可满足绝大多数Scala应用的运行需求 - 以fat jar方案为例,对应Dockerfile参考:
分层打包方案的Dockerfile参考:FROM eclipse-temurin:17-jre-jammy WORKDIR /app COPY service-a/target/scala-2.13/service-a-1.0.0.jar app.jar ENTRYPOINT ["java", "-jar", "app.jar"]FROM eclipse-temurin:17-jre-jammy WORKDIR /app COPY service-a/target/universal/stage/ ./ ENTRYPOINT ["bin/service-a"]
内容的提问来源于stack exchange,提问作者Kotesh Meesala
相关产品推荐
相关产品推荐

