Computer scienceBackendKtorKtor Advanced

Ktor Containerizing

7 minutes read

Containerization is a technique that encapsulates an application and its dependencies into a container. This container can be thought of as a self-sufficient unit that can run anywhere, regardless of the environment specifics. Docker, an open-source project, has popularized this technology. It provides a platform and tools for creating and managing containers, ensuring they are lightweight and portable.

Installing the Ktor Plugin

The Ktor plugin is a tool that simplifies the process of creating Docker images for your Ktor applications. To install it, you need to add the following lines to your build.gradle.kts file:

plugins {
    id("io.ktor.plugin") version "2.3.0"
}

Configuring the Ktor Plugin

The Ktor plugin provides a range of configuration options to tailor the Docker image creation process to your specific needs. These configurations are set in the build.gradle.kts file, using the ktor.docker extension. Let's explore these configurations in detail:

The jreVersion property is used to define the Java Runtime Environment (JRE) version that the Docker image should use. Ensuring the correct JRE version is crucial for the smooth operation of your application.

For instance, to set the JRE version to 17, you would use:

ktor {
    docker {
        jreVersion.set(io.ktor.plugin.features.JreVersion.JRE_17)
    }
}

In Docker, an image name is a human-readable identifier for your Docker image. When you build a Docker image, you give it a name so that you can refer to it when running containers or pushing it to a Docker registry.

A tag is a mechanism in Docker that allows you to give versioning information to your Docker images. For example, you might have a Docker image for your application that has different versions like 1.0, 1.1, 2.0, and so on. Using tags, you can pull, run, or distribute the specific version of the application you need.

With the localImageName and imageTag properties, you can assign a unique identifier and version to your Docker image. This is particularly useful when managing multiple images or versions of the same application. For example, to name your image "ktor-with-docker" and tag it as "1.0.0", you would write:

ktor {
    docker {
        localImageName.set("ktor-with-docker")
        imageTag.set("1.0.0")
    }
}

A Docker registry is a storage and distribution system for Docker images. It's where your Docker images are pushed and pulled from. Registries can be public or private. Public registries are hosted by organizations for public use, like Docker Hub or Google Container Registry. Private registries are typically hosted by organizations for their internal use.

An external registry is a Docker registry that is hosted outside your organization's infrastructure. It's accessible over the internet and can be used to distribute Docker images to a wide audience. Docker Hub and Google Container Registry are examples of external registries.

The externalRegistry property is for specifying an external registry for the publishImage task. This becomes necessary when you want to distribute your Docker image to a wider audience or need to use services like Docker Hub or Google Container Registry. Here is an example of the configuration the Docker Hub registry:

ktor {
    docker {
        externalRegistry.set(
            io.ktor.plugin.features.DockerImageRegistry.dockerHub(
                appName = provider { "ktor-with-docker" },
                username = "USERNAME",
                password = "PASSWORD"
            )
        )
    }
}

Port mapping is a crucial feature when working with Docker. It allows communication between the Docker container and the outside world. When a Docker container is created, it has its own set of ports. If a service inside the container is set to communicate on a specific port, that port is not automatically available to the host machine or network.

To make it available, we need to map the container's port to a port on the host machine. The portMappings property allows you to change the ports that the runDocker task publishes. This is useful if your server is configured to use a port other than the default 8080. Here's how you can map the 8080 container port to the 80 Docker host port:

ktor {
    docker {
        portMappings.set(listOf(
            io.ktor.plugin.features.DockerPortMapping(
                80, 8080, io.ktor.plugin.features.DockerPortMappingProtocol.TCP
            )
        ))
    }
}

Understanding Plugin Tasks

The Ktor plugin provides several tasks that automate various aspects of packaging, running, and deploying your application:

  • buildImage: This task builds a Docker image of your project and saves it as a tarball in the build directory. Then you can load this image into a Docker daemon using the docker load command. For example, after executing the commands below, you will see the output in the screenshot:

    gradlew buildImage
    docker load < .\build\jib-image.tar

    Now the corresponding image will appear in the docker image list:

  • publishImageToLocalRegistry: This task builds a Docker image of your project and publishes it to a local Docker registry. This task is equivalent to the execution of both gradlew buildImage and docker load. To start the container, use the following command:

    docker run -p 80:8080 ktor-with-docker:1.0.0

    Note that via the -p flag we specify the ports that will be open for the container. The syntax is: -p <external port>:<port inside the container>:

  • publishImage: This task builds a Docker image of your project and publishes it to an external Docker registry, such as Docker Hub or Google Container Registry.

  • runDocker: This task builds a Docker image of your project, loads it into a Docker daemon, and runs it. By default, the Ktor server will respond on http://0.0.0.0:8080. This task is equivalent to the execution of both gradlew publishImageToLocalRegistry and docker run:

Manual Image Configuration

In some cases, you may want to have more control over how your Docker image is built. This is where the Dockerfile comes in. Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image. In other words, it is used to define how a Docker image should be built.

Here's an example of a Dockerfile for a Ktor application:

FROM gradle:7-jdk11 AS build
COPY --chown=gradle:gradle . /home/gradle/src
WORKDIR /home/gradle/src
RUN gradle buildFatJar --no-daemon

FROM openjdk:11
EXPOSE 8080:8080
RUN mkdir /app
COPY --from=build /home/gradle/src/build/libs/*.jar /app/ktor-with-docker.jar
ENTRYPOINT ["java","-jar","/app/ktor-with-docker.jar"]

This Dockerfile is divided into two parts:

  • The first part is the build stage. It starts with a Gradle image with JDK 11 installed (gradle:7-jdk11). Then it copies the source code into the image and uses Gradle to build a 'fat' JAR file that contains the application and all its dependencies.

  • The second part is the run stage. It starts with an OpenJDK 11 image (openjdk:11). It then exposes port 8080, creates a directory to hold the application, and copies the 'fat' JAR file from the build stage into this directory. Finally, it specifies that the application should be run by executing java -jar /app/ktor-with-docker.jar.

Conclusion

Containerization with tools like Docker provides a flexible way to deploy applications. The Ktor plugin for Gradle further simplifies this process for Ktor applications, providing tasks for building and running Docker images, and allowing for detailed configuration. By understanding these tools and how to use them, you can streamline your application deployment process and ensure your applications run reliably in any environment.

How did you like the theory?
Report a typo