Ktor is a lightweight, asynchronous web framework built by JetBrains for Kotlin. It leverages Kotlin coroutines for non-blocking I/O and offers a clean DSL for routing, serialization, and authentication. This guide covers building and deploying a Ktor application on a VPS.
Project Setup
# Use the Ktor Project Generator at https://start.ktor.io
# Or create manually with Gradle
mkdir ktor-app && cd ktor-app
# build.gradle.kts
plugins {
kotlin("jvm") version "2.0.0"
id("io.ktor.plugin") version "2.3.12"
kotlin("plugin.serialization") version "2.0.0"
}
application {
mainClass.set("com.example.ApplicationKt")
}
dependencies {
implementation("io.ktor:ktor-server-core-jvm")
implementation("io.ktor:ktor-server-netty-jvm")
implementation("io.ktor:ktor-server-content-negotiation-jvm")
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")
implementation("io.ktor:ktor-server-cors-jvm")
implementation("io.ktor:ktor-server-compression-jvm")
implementation("io.ktor:ktor-server-call-logging-jvm")
implementation("io.ktor:ktor-server-status-pages-jvm")
implementation("org.jetbrains.exposed:exposed-core:0.53.0")
implementation("org.jetbrains.exposed:exposed-dao:0.53.0")
implementation("org.jetbrains.exposed:exposed-jdbc:0.53.0")
implementation("org.postgresql:postgresql:42.7.3")
implementation("com.zaxxer:HikariCP:5.1.0")
implementation("ch.qos.logback:logback-classic:1.5.6")
}
Application Code
// src/main/kotlin/com/example/Application.kt
package com.example
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.response.*
import io.ktor.server.request.*
import io.ktor.server.routing.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.plugins.compression.*
import io.ktor.server.plugins.calllogging.*
import io.ktor.server.plugins.statuspages.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.http.*
import kotlinx.serialization.Serializable
@Serializable
data class User(val id: Int, val name: String, val email: String)
@Serializable
data class CreateUser(val name: String, val email: String)
@Serializable
data class ErrorResponse(val error: String)
fun main() {
embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
configurePlugins()
configureRouting()
}.start(wait = true)
}
fun Application.configurePlugins() {
install(ContentNegotiation) { json() }
install(CORS) { anyHost(); allowHeader(HttpHeaders.ContentType) }
install(Compression) { gzip() }
install(CallLogging)
install(StatusPages) {
exception<Throwable> { call, cause ->
call.respond(HttpStatusCode.InternalServerError,
ErrorResponse(cause.localizedMessage ?: "Unknown error"))
}
}
}
fun Application.configureRouting() {
routing {
get("/health") {
call.respond(mapOf("status" to "healthy"))
}
route("/api/users") {
get {
val users = listOf(
User(1, "Alice", "alice@example.com"),
User(2, "Bob", "bob@example.com")
)
call.respond(users)
}
post {
val input = call.receive<CreateUser>()
val user = User(3, input.name, input.email)
call.respond(HttpStatusCode.Created, user)
}
get("/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
?: return@get call.respond(HttpStatusCode.BadRequest,
ErrorResponse("Invalid ID"))
call.respond(User(id, "Alice", "alice@example.com"))
}
}
}
}
Building and Deploying
# Build fat JAR
./gradlew buildFatJar
# Output: build/libs/ktor-app-all.jar
# Or build with Ktor plugin
./gradlew installDist
# Output: build/install/ktor-app/
# Copy to VPS
scp build/libs/ktor-app-all.jar user@vps:/opt/ktor-app/
# Run on VPS
java -jar /opt/ktor-app/ktor-app-all.jar
Docker Deployment
FROM gradle:8-jdk21 AS builder
WORKDIR /app
COPY . .
RUN gradle buildFatJar --no-daemon
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /app/build/libs/*-all.jar app.jar
USER nobody
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
Systemd Service
[Unit]
Description=Ktor Application
After=network.target
[Service]
Type=simple
User=appuser
ExecStart=/usr/bin/java -jar /opt/ktor-app/ktor-app-all.jar
Environment=JAVA_OPTS=-Xmx512m
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Database with Exposed ORM
// Database configuration using Exposed
object Users : Table() {
val id = integer("id").autoIncrement()
val name = varchar("name", 100)
val email = varchar("email", 255).uniqueIndex()
override val primaryKey = PrimaryKey(id)
}
fun initDatabase() {
Database.connect(
HikariDataSource(HikariConfig().apply {
jdbcUrl = System.getenv("DATABASE_URL")
maximumPoolSize = 20
isAutoCommit = false
})
)
transaction { SchemaUtils.create(Users) }
}
// In routes
get("/api/users") {
val users = transaction {
Users.selectAll().map {
User(it[Users.id], it[Users.name], it[Users.email])
}
}
call.respond(users)
}
Summary
Ktor provides an idiomatic Kotlin experience for building web APIs with coroutine-based concurrency. Its plugin architecture keeps applications lightweight — you only include what you need. Combined with Kotlin serialization for type-safe JSON handling and Exposed for database access, Ktor offers a productive, performant stack for JVM-based web services. Deployment follows the standard JVM pattern: build a fat JAR, deploy with systemd, and proxy with Nginx.