Back to Blog
Tutorial10 min read2026-07-03

How to Deploy a Ktor Kotlin Backend

Ktor is JetBrains' lightweight, coroutine-based Kotlin framework for building asynchronous servers. Learn how to package it as a fat JAR or container and deploy it to production with a managed database.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Ktor is JetBrains' own framework for building asynchronous servers and clients in Kotlin. It's lightweight, idiomatic, coroutine-first, and avoids the heavy annotation-driven magic of traditional JVM frameworks. If you want a modern Kotlin backend without dragging in the full Spring stack, Ktor is a fantastic fit. Deploying it is straightforward once you decide how to package the JVM app. This guide covers both fat-JAR and container approaches.

How Ktor is structured

A Ktor app is configured either in code or via an application.conf (HOCON) file. You install *features* (plugins) — content negotiation, authentication, call logging, CORS — and define routing. It runs on an engine (Netty is the common choice). The deployment question is purely "how do I get the JVM to run my app in a container," and there are two clean answers.

Step 1: Read the port from the environment

Whatever else you do, the app must listen on the platform-provided port and bind to all interfaces. With the embedded server:

import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.application.*
import io.ktor.server.routing.*
import io.ktor.server.response.*

fun main() {
    val port = System.getenv("PORT")?.toInt() ?: 8080
    embeddedServer(Netty, port = port, host = "0.0.0.0") {
        routing {
            get("/health") { call.respondText("ok") }
        }
    }.start(wait = true)
}

If you use application.conf instead, parameterize the port:

ktor {
  deployment { port = 8080, port = ${?PORT} }
  application { modules = [ com.example.ApplicationKt.module ] }
}

Step 2: Choose your packaging

ApproachHowProsCons
Fat JARShadow/Assembly plugin builds one runnable JARSimple, portableNeed a JRE base image
Multi-stage containerGradle builds inside DockerReproducible buildsSlightly more Dockerfile

Most teams build a fat JAR (a single JAR with all dependencies) and run it on a slim JRE image. The Gradle Shadow plugin makes this one task.

Step 3: Build a fat JAR

With the Shadow plugin in build.gradle.kts:

plugins {
    application
    id("com.github.johnrengelman.shadow") version "8.1.1"
}
application {
    mainClass.set("com.example.ApplicationKt")
}

Then ./gradlew shadowJar produces build/libs/your-app-all.jar.

Step 4: The Dockerfile (multi-stage)

Build with the JDK, run on a slim JRE:

# ---- build stage ----
FROM gradle:8.10-jdk21 AS build
WORKDIR /app
COPY . .
RUN gradle shadowJar --no-daemon

# ---- runtime stage ----
FROM eclipse-temurin:21-jre-jammy
WORKDIR /app
COPY --from=build /app/build/libs/*-all.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

Using a JRE (not JDK) runtime image keeps it lean. Temurin's JRE images are a solid, well-maintained base.

Step 5: Tune the JVM for containers

Modern JVMs are container-aware, but it's wise to be explicit so the heap respects your memory limit:

ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]

MaxRAMPercentage tells the JVM to size its heap relative to the container's memory limit, avoiding OOM kills from an oversized heap. This is the single most important JVM-in-a-container setting.

Step 6: Add a database

For Postgres, Exposed (JetBrains' Kotlin SQL framework) or plain HikariCP + JDBC both work. Read the connection string from the environment:

import com.zaxxer.hikari.*

val config = HikariConfig().apply {
    jdbcUrl = System.getenv("DATABASE_URL")   // jdbc:postgresql://...
    maximumPoolSize = 5
    driverClassName = "org.postgresql.Driver"
}
val dataSource = HikariDataSource(config)

Note: JDBC expects a jdbc:postgresql://... URL. If your platform injects a postgres://... URL, transform it (prepend jdbc: and move credentials to properties) at startup.

Step 7: Deploy on PandaStack

  1. 1Provision a managed PostgreSQL (16.x).
  2. 2Create a container app from your repo. PandaStack builds the Dockerfile with rootless BuildKit and streams live build logs while Gradle compiles.
  3. 3Link the database — a connection string is auto-injected; adapt it to JDBC form if needed.
  4. 4Set JAVA_OPTS / JVM flags as needed and ensure the app reads PORT.
  5. 5Attach a custom domain with automatic SSL.

Alternatively, if you prefer buildpacks, PandaStack auto-detects Gradle/JVM projects — but the explicit Dockerfile gives you full control over the JRE base and JVM flags.

Production polish

  • Call logging — install the CallLogging plugin so requests appear in live logs.
  • Content negotiation — install ContentNegotiation with kotlinx.serialization for JSON.
  • Graceful shutdown — Ktor handles SIGTERM; configure a shutdown grace period so deploys drain requests.
  • Health endpoint — keep /health cheap for readiness probes.

Honest caveats

The JVM trade-offs apply: compared to a Rust or Go binary, a Ktor container carries a JVM, uses more baseline memory, and has a slower cold start (JVM warm-up). For scale-to-zero free-tier scenarios, that cold-start cost is real — though Ktor is much lighter than Spring Boot, so it's a relatively fast-starting JVM app. The upside is the rich JVM ecosystem and Kotlin's excellent developer experience. Set MaxRAMPercentage and give the container adequate memory and you'll have a smooth ride.

Wrapping up

Deploying Ktor is a clean JVM story: build a fat JAR (or multi-stage container), run it on a slim JRE, set MaxRAMPercentage, bind to PORT/0.0.0.0, and connect to managed Postgres via HikariCP. You get a lightweight, idiomatic Kotlin backend without framework bloat.

PandaStack builds your Dockerfile (or auto-detects Gradle), auto-wires a managed database, and serves with automatic SSL — and the free tier is enough to run a real Ktor service. Deploy yours at https://dashboard.pandastack.io.

References

  • Ktor documentation: https://ktor.io/docs/
  • Ktor deployment / fat JAR: https://ktor.io/docs/server-fatjar.html
  • Ktor in Docker: https://ktor.io/docs/docker.html
  • JVM container awareness (MaxRAMPercentage): https://docs.oracle.com/en/java/javase/21/docs/specs/man/java.html
  • HikariCP connection pooling: https://github.com/brettwooldridge/HikariCP

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also