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:
Nextly, let’s click the “Add plugins” button and add the “Routing” plugin:
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.
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:
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.