Advantages

Here are the key advantages of this approach:

object backend extends Common with ScalafmtModule with ScalafixModule {

  def ivyDeps =
    super.ivyDeps() ++ Config.jvmDependencies ++ Config.sharedDependencies

  def moduleDeps = Seq(shared.jvm)

  def frontendResources = T{PathRef(frontend.fullLinkJS().dest.path)}

  def staticAssets = T.source{PathRef(frontend.millSourcePath / "ui")}

  def allClasspath = T{localClasspath() ++ Seq(frontendResources()) ++ Seq(staticAssets())  }

  override def assembly: T[PathRef] = T{
    Assembly.createAssembly(
      Agg.from(allClasspath().map(_.path)),
      manifest(),
      prependShellScript(),
      Some(upstreamAssembly2().pathRef.path),
      assemblyRules
    )
  }
}

A dockerfile that uses the assembly:

FROM azul/zulu-openjdk-alpine:17

# See the GHA for building the assembly
COPY "./out/backend/assembly.dest/out.jar" "/app/app.jar"

EXPOSE 8080

ENTRYPOINT [ "java", "-jar", "/app/app.jar" ]

Here is a GHA, that deploys that assembly to fly.io.

name: Deploy to fly.io

on:
  push:
    branches: [main]

env:
  FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
      - uses: coursier/setup-action@main
        with:
          jvm: temurin@17
          apps: mill
      - uses: superfly/flyctl-actions/setup-flyctl@master
      - name: Build application
        run: mill show backend.assembly -j 0
      - name: Deploy to fly.io
        run: flyctl deploy --remote-only

You are live.