Easily Deploy Your Ktor Server with Docker

If you've ever been wondering how to deploy your Ktor Server app using Docker, then this step-by-step guide will be the right choice.
Image shows Ktor logo in the foreground and a blurred photo of a desk with two monitors.

Introduction

Hello and welcome to my next step-by-step guide, in which we will learn how to deploy a Ktor Server application using Docker.

Firstly, we will prepare a simple app with a REST endpoint and a database connection. This way, we will see one of the most common issues and how we can deal with it.

Finally, I will show you three approaches you can use in your project.

Create a Ktor Server Skeleton

So, as the first step, let’s navigate to the https://start.ktor.io/ page and generate a Ktor Server project.

Note: If you are using IntelliJ IDEA Ultimate Edition, then you can generate a new project with it, as well.

Please specify whatever values you want, except the “Configuration in”- let’s stick to the HOCON file to be on the same page:

Image is a screenshot from the Ktor Project Generator page showing the values author put in his project.

Nextly, let’s click the “Add plugins” button and add the “Routing” plugin:

Image is a screenshot showing the Routing plugin selected when generating a new Ktor Server project.

After that, let’s hit the “Generate project” button, save it to our machine, and import it to the IDE (for example, IntelliJ IDEA).

Configure Database Connection

Although with all of that done we are ready to learn how to deploy a Ktor server with Docker, let me show you one more thing. Most of the time, when creating server-side applications, we will have to connect to some database. And then, we can easily end up with connection issues, which I would like to show you in this tutorial.

Of course, if that’s not the case in your project, then please feel free to skip this part. Otherwise, I do recommend going through this part and adjusting it to your needs.

Configure Imports

Firstly, let’s modify the build.gradle.kts file:

val mysql_connector_version: String by project
val exposed_version: String by project

dependencies {
  // other dependencies
  implementation("org.jetbrains.exposed:exposed-core:$exposed_version")
  implementation("org.jetbrains.exposed:exposed-dao:$exposed_version")
  implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version")

  implementation("com.mysql:mysql-connector-j:$mysql_connector_version")
}

Of course, with this approach we need to add two more lines to gradle.properties, as well:

mysql_connector_version=8.0.31
exposed_version = 0.41.1

As we can see, we just added the Exposed– an ORM framework for Kotlin- and MySQL Connector/J– a JDBC Type 4 driver for MySQL.

Image shows two ebooks people can get for free after joining newsletter

Database Config

As the next step, let’s add a couple of lines to the application.conf:

db {
  url = ${DB_URL}
  user = ${DB_USER}
  password = ${DB_PASSWORD}
  driver = "com.mysql.cj.jdbc.Driver"
}

As can be seen, we externalized the URL, user, and password settings to the environment variables.

This way, we can easily provide different values (for example, for different environments) without modifying the code. Nevertheless, we don’t do that for the driver, as this should be constant across all envs.

Following, let’s head to the plugins directory and create the Database.kt file:

fun Application.configureDatabase() {
  val url = environment.config.property("db.url").getString()
  val user = environment.config.property("db.user").getString()
  val password = environment.config.property("db.password").getString()
  val driverClassName = environment.config.property("db.driver").getString()

  Database.connect(
      url = url,
      driver = driverClassName,
      user = user,
      password = password
  )

  transaction {
      AppUser.all()
          .toList()
  }
}

object AppUsers : IntIdTable(name = "app_user") {
  val email = varchar("email", length = 255).uniqueIndex()
}

class AppUser(id: EntityID) : IntEntity(id) {
  companion object : IntEntityClass(AppUsers)

  var email by AppUsers.email
}

To put it simply, the above code is responsible for making a connection to the database based on values from application.conf file and fetching all users from the “app_user” table.

Verification

With all of that being done, we can run the application and verify the output.

If we don’t pass any environment variables, we will see the following:

Exception in thread “main” com.typesafe.config.ConfigException$UnresolvedSubstitution: application.conf Could not resolve substitution to a value: ${DB_PASSWORD}

Process finished with exit code 1

In IntelliJ, we can set variables by editing the running configuration:

Screenshot shows how to set up environment variables for project in IntelliJ.

Please note that values should not be quoted (example: “value”), otherwise, we can end up with something like this:

Exception in thread “main” java.lang.IllegalStateException: Can’t resolve dialect for connection: …
Process finished with exit code 1

So, if everything is set correctly, we should see the following:

# Logs:
INFO Application - Autoreload is disabled because the development mode is off.
DEBUG Exposed - SELECT app_user.id, app_user.email FROM app_user
INFO Application - Application started in 0.801 seconds.
INFO Application - Responding at http://127.0.0.1:8080

# And for request:
GET http://localhost:8080/

# Response:
Hello World!

And although we didn’t do anything, the Routing.kt file with this endpoint was added automatically when generating the project.

Deploy Ktor Server With Docker – Manual Approach

So finally, we can see the first- manual- approach with which we can deploy the Ktor app with Docker.

Create Dockerfile

In my articles, I focus on the practice and getting things done.

However, if you would like to get a strong understading of DevOps concepts, which are more and more in demand nowadays, then I highly recommend to check out KodeKloud courses, like this Docker and Kubernetes learning paths.

To do so, let’s a file named Dockerfile to the root of the project:

FROM openjdk:17-jdk-alpine3.14
RUN mkdir /app
COPY ./build/libs/com.codersee.ktor-docker-all.jar /app/app.jar
ENTRYPOINT ["java","-jar","/app/app.jar"]

As we can see, we will use the OpenJDK alpine image and run a built jar.

Build The Image

As the next step, let’s build a jar file using a gradle wrapper:

./gradlew build

Note: if the build is failing, then please navigate to the test directory and comment out the test.

If everything is fine, we should see the “BUILD SUCCESSFUL” message and we can create a Docker image:

docker build -t my-example-app .

# Verification: 
docker images

# Result:
REPOSITORY     TAG    IMAGE ID     CREATED       SIZE
my-example-app latest f50b5432ea63 9 seconds ago 346MB

As we can see, the image was created successfully and we can proceed.

Docker Run

With all of that done, we can deploy our Ktor Server using the docker run command:

docker run -p 8080:8080 --name example-container -e DB_URL=jdbc:mysql://host.docker.internal:3306/boards -e DB_USER=root -e DB_PASSWORD=p@ssword my-example-app

# Output: 
2023-01-13 16:30:43.854 [main] INFO Application - Autoreload is disabled because the development mode is off.
2023-01-13 16:30:44.537 [main] DEBUG Exposed - SELECT app_user.id, app_user.email FROM app_user
2023-01-13 16:30:44.543 [main] INFO Application - Application started in 0.719 seconds.
2023-01-13 16:30:44.746 [DefaultDispatcher-worker-1] INFO Application - Responding at http://0.0.0.0:8080

As can be seen, the server is up and running, which means that it successfully connected to our MySQL instance.

Note: Please note that when working with Docker on your local machine, you can’t provide localhost as a host value for the connection anymore.

Nevertheless, the host.docker.internal is supported for:

  • Docker-for-mac or Docker-for-Windows 18.03+
  • Docker-for-Linux 20.10.0+ (if you started your Docker container with --add-host host.docker.internal:host-gateway)

Of course, there are more ways to go in such a case, but this is the simplest one in my opinion.

Deploy Ktor Server With Docker – Hybrid Approach

With that covered, let’s figure out another approach to deploy the Ktor app using Docker.

This time, we will build a docker image to a .tar file using the Ktor plugin. If you generated the project with me, then we don’t need to do anything. Otherwise, please remember to add it to the build.gradle.kts file:

plugins {
  // Other plugins
  id("io.ktor.plugin") version "2.2.2"
}

Create New Image

And as the next step, let’s run the buildImage command:

./gradlew buildImage

As a result, we should see the jib-image.tar file inside the build directory.

If that’s the case, then let’s load an image from a tar archive with docker load:

docker load -i build/jib-image.tar

# Verification: docker images # Result: 
REPOSITORY        TAG    IMAGE ID     CREATED      SIZE
ktor-docker-image latest 31b35e55ec24 53 years ago 286MB

And although it was not created 53 years ago 🙂 , we can clearly see that the size is reduced compared to the previous approach.

Deploy Server

And finally, let’s verify again.

But this time, we need to specify the ktor-docker-image as the image name:

docker run -p 8080:8080 --name example-container -e DB_URL=jdbc:mysql://host.docker.internal:3306/boards -e DB_USER=root -e DB_PASSWORD=p@ssword ktor-docker-image

# Output: 
2023-01-13 16:30:43.854 [main] INFO Application - Autoreload is disabled because the development mode is off.
2023-01-13 16:30:44.537 [main] DEBUG Exposed - SELECT app_user.id, app_user.email FROM app_user
2023-01-13 16:30:44.543 [main] INFO Application - Application started in 0.719 seconds.
2023-01-13 16:30:44.746 [DefaultDispatcher-worker-1] INFO Application - Responding at http://0.0.0.0:8080

Customization

Of course, we can customize the image name and tag, as well.

To do so, let’s add the following inside the build.gradle.kts:

ktor {
  docker {
    localImageName.set("my-custom-image-name")
    imageTag.set("alpha")
  }
}

So this time, when we repeat the previous steps, we should see the following image my-custom-image-name:alpha.

Deploy Ktor Server With Docker – Built-in Approach

Lastly, I just wanted to mention that the Ktor plugin comes not only with the buildImage task when it comes to Docker.

With this plugin, we can make use of 3 more tasks:

  • publishImageToLocalRegistry – which is a combination of the buildImage and gradle load from the previous step paragraph.
  • publishImage– which takes care of pushing to an external registry.
  • runDocker – which additionally launches the Ktor server to listen on the 8080 port of localhost (unless we specify otherwise).

And although I show this approach as the last one in this tutorial, in my opinion, we should delegate the deployment process to these tasks as often, as it is possible.

Summary

And that’s all for this tutorial on how to deploy a Ktor Server with Docker. Together, we’ve learned 3 approaches, which can help us do that with ease and as always, you can find the source code on GitHub.

If you enjoyed the content or just would like to share your feedback or thoughts, then let me know in the comments section 🙂

Lastly, if you would like to learn a bit more about Ktor, then check out my other articles about Ktor with MongoDB, or Ktor with PostgreSQL.

Share this:

Related content

Newsletter
Image presents 3 ebooks with Java, Spring and Kotlin interview questions.

Never miss any important updates from the Kotlin world and get 3 ebooks!

You may opt out any time. Terms of Use and Privacy Policy