Scala Native HTTP servers
in a single executable

SNUnit runs your Scala Native web app on FreeUnit. Use the libraries you already know — http4s, Tapir, Cask, Undertow, endpoints4s — and get process management, load balancing and restarts for free.

curl -fsSL https://raw.githubusercontent.com/lolgab/snunit/main/install.sh | bash

Hello world, no build file

Write one Scala file with a //> using dep line for SNUnit and run it. The snunit command line tool builds with Scala Native and serves on port 8080.

  • snunit run Hello.scala builds and serves
  • snunit run -w rebuilds and restarts on every change
  • snunit package produces the executable to deploy
//> using dep com.github.lolgab::snunit::0.13.0
import snunit.*

@main
def run =
  SyncServerBuilder
    .setRequestHandler(req =>
      req.send(
        statusCode = StatusCode.OK,
        content = "Hello world!\n",
        headers = Headers("Content-Type" -> "text/plain")
      )
    )
    .build()
    .listen()

Why SNUnit

A small Scala Native library on top of a battle-tested application server.

๐Ÿ“ฆSingle executable

Your app embeds unitd, starts it with a generated configuration and runs itself as the application. Nothing to install on the server.

๐Ÿ”Restarts & load balancing

FreeUnit restarts crashed processes and balances requests across multiple application processes, static or dynamic.

โšกNative performance

Compiled ahead of time with Scala Native: tiny memory footprint and instant startup, with no JVM involved.

๐Ÿ”€Sync or async

Simple blocking servers with Cask, Undertow or endpoints4s, or cats-effect with epoll/kqueue in http4s and Tapir to handle many concurrent requests per process.

๐Ÿ”ŒWebSockets

Supported by the http4s and Tapir integrations.

โš™๏ธTyped configuration

UnitConfig covers listeners and TLS, routes, upstreams, HTTP settings, processes, limits, isolation and access logs, all from Scala code.

๐Ÿ“ˆObservability included

OpenTelemetry traces, metrics and logs, and snunit run --stack for dashboards, traces and logs running inside FreeUnit. See it.

๐Ÿ›‘Graceful shutdown

On SIGTERM, SIGINT or SIGHUP in-flight requests finish before exit. A watchdog cleans up even after SIGKILL.

๐ŸงฐBring your build tool

Use snunit with scalino, or add unitd to an executable built with sbt, Mill or scala-cli.

๐Ÿ–ฅ๏ธLinux & macOS

Prebuilt FreeUnit for Linux and macOS, x86_64 and aarch64, downloaded and cached on first use.

Use the libraries you love

Pick your style. Same single-executable deployment.

Extend snunit.Http4sApp and implement routes. Based on the epollcat event loop. snunit-http4s

//> using dep com.github.lolgab::snunit-http4s0.23::0.13.0
//> using dep org.http4s::http4s-dsl::0.23.38
import cats.effect.*
import org.http4s.*
import org.http4s.dsl.io.*

object app extends snunit.Http4sApp {
  def routes = Resource.pure(
    HttpRoutes
      .of[IO] { case GET -> Root =>
        Ok("Hello from SNUnit Http4s!")
      }
      .orNotFound
  )
}

Typed configuration

Configure FreeUnit from Scala, no JSON. Http4sApp and TapirApp expose a unitConfig you override as a ResourceIO; it covers listeners and TLS, routes, upstreams, HTTP settings, application processes and limits, isolation, access log and telemetry.

//> using dep com.github.lolgab::snunit-http4s0.23::0.13.0
//> using dep org.http4s::http4s-dsl::0.23.38
import cats.effect.*
import org.http4s.*
import org.http4s.dsl.io.*
import snunit.config.*
import scala.concurrent.duration.*

object app extends snunit.Http4sApp {
  override def unitConfig = Resource.pure(
    UnitConfig(
      listeners = Seq(Listener.port(9000)),
      application = Application(
        processes = Some(Processes.Dynamic(max = 8, spare = 2)),
        limits = Some(Limits(timeout = Some(30.seconds)))
      ),
      http = HttpSettings(maxBodySize = Some(1024 * 1024)),
      shutdownTimeout = 20.seconds
    )
  )

  def routes = Resource.pure(
    HttpRoutes.of[IO] { case GET -> Root => Ok("Hello!") }.orNotFound
  )
}

TapirApp works the same way: override def unitConfig.

unitConfig is a ResourceIO[UnitConfig] and UnitConfig is a plain case class, so load values with cats.effect.std.Env, from a file or anywhere else:

override def unitConfig = Resource.eval(
  Env[IO].get("PORT").map { port =>
    UnitConfig().withPort(port.fold(8080)(_.toInt))
  }
)

HTTPS takes certificate bundles by name:

UnitConfig(
  listeners = Seq(Listener(address = "*:8443", tls = Some(Tls(Seq("main"))))),
  certificates = Map("main" -> scala.io.Source.fromFile("bundle.pem").mkString)
)

Observability, built in

One command runs your app with dashboards, traces, logs and metrics, all running inside FreeUnit next to it, already connected. No Docker, no YAML, nothing to wire.

snunit run --stack Hello.scala

The first time, snunit stack install downloads the programs (checksums verified).

localhost:3000
Dashboard with the request rate, connections and application processes of an SNUnit app

A dashboard with the request rate, open connections and application processes, plus links to the traces and logs.

๐Ÿ“ŠDashboards

Request rate, connections and processes at a glance, provisioned for you. Built from Perses.

๐ŸงตTraces

One span per request. The traceparent header reaches your app, so an OpenTelemetry SDK joins the same trace. Built from Jaeger.

๐Ÿ“œLogs

JSON access log, FreeUnit log and your app's output, shipped and searchable. Built from VictoriaLogs.

โฑ๏ธMetrics

Connections, requests, processes and sidecars as snunit_* metrics, scraped for you. Built from Prometheus.

UnitConfig(
  observability = Some(
    Observability(
      "my-service",
      stack = Some(Stack())
    )
  )
)

// pass it to SyncServerBuilder.setConfig,
// or override unitConfig in Http4sApp / TapirApp

The same from code, where you can also keep the data on disk (dataDirectory) and choose the ports. Already have a stack? Point otlpEndpoint to your OTLP receiver, logs to a Loki-compatible server and let your Prometheus scrape the metrics endpoint.

Deploying? snunit stack install --dir /opt/snunit-stack in your image and set SNUNIT_STACK_DIR.

The programs are modified builds of Prometheus, Jaeger, VictoriaLogs and Perses (all Apache-2.0) that serve their HTTP API through FreeUnit. They are not the official releases and are not made, endorsed or supported by their authors; the names belong to their owners. Archives are named after what they do, with the licenses of every dependency inside. See the licenses and the build scripts. They listen on localhost and have no login: put your own authentication in front before exposing them.

How it works

Start the executable and it launches FreeUnit, which launches the same executable as its application.

Your executablestarted directly
โ†’ extracts & starts โ†’
unitdFreeUnit router
โ†’ spawns & balances โ†’
Your executableN app processes

Get started

From zero to a running server in three commands.

Install scalino and the snunit CLI

curl -fsSL https://raw.githubusercontent.com/lolgab/snunit/main/install.sh | bash

Write Hello.scala and run it

snunit run Hello.scala      # serves on http://localhost:8080
snunit run -w Hello.scala   # rebuild and restart on every change

Package it and deploy anywhere

snunit package Hello.scala -o hello
./hello

See the README for using another build tool, sync vs. async servers and the details of each integration.