How to Publish a Ktor Docker Image to Artifact Registry Using the Ktor Plugin

In this post, we’ll walk through the configuration of the Ktor plugin to deploy a Ktor Docker image to Artifact Registry using Gradle.  We’ll also explore several other Gradle tasks that make working with a containerized version of our Ktor service easier.

This post is part of a series on managing Ktor service deployments using Gradle:

Ktor is a framework from JetBrains enabling developers to build client and/or server applications using Kotlin.  For organizations using Kotlin for server-side development, Ktor is an appealing framework as it’s built from the ground up to leverage Kotlin language features such as coroutines. 

In many organizations today, especially those undergoing “cloud transformation”, containers are a popular choice for packaging, sharing, and deploying applications.  Those containers can be used with tools like Docker to simplify the development, testing, and coordination of services.

Containers are a key aspect of many serverless deployment workflows within Google Cloud Platform.  Servless solutions like Cloud Run rely on containers to respond quickly to increases in server load by deploying new containers, when needed, to handle the incoming traffic, and then spin down those containers when traffic subsides.

If using Ktor for serverless applications, you’ll likely need to create a container image for your application. Once you create an image, you’ll need to deploy it to an external registry so it can eventually be deployed to a container and start receiving traffic.

It’s possible to do this in an entirely manual fashion; defining our own Dockerfile, building the image locally, and then pushing it to some external registry such as Google’s Artifact Registry.

However, the Ktor plugin provides mechanisms for simplifying this process in a way that makes it easy for those without extensive Docker, container, or GCP knowledge to leverage.

If you need to publish your Ktor Docker image to Container Registry, check out this related post.

Prerequisites

Since we’ll be deploying our Ktor service container to Artifact Registry, that comes with a new Google Cloud Platform (GCP) prerequisites.

  1. We need to have access to a GCP organization
    1. How to setup a GCP organization?
    2. You’ll eventually need to set up a billing account for your organization.  To try out GCP for learning purposes, there are often ways to find free, or discounted, billing credits
      1. Trial period with $300 free credits for new users
      2. GCP credits for faculty and students
      3. Google Cloud Innovators Plugs program
  2. We need to have a project set up within that organization
    1. How to create and manage GCP projects?
  3. Artifact Registry must be enabled within that project
    1. How to enable Artifact Registry for a GCP project?

How to configure the Ktor plugin to build a Docker image?

To configure a Docker image, and publishing tasks, for our Ktor project, we’ll use the Ktor plugin dsl in our project’s Gradle buildscript.  This will leverage the jib Gradle plugin under the hood to configure and build images for your project.

// build.gradle.kts
ktor {
   docker {
      // container configuration goes here
   }
}

To start, we’ll specify a Java Runtime Environment (JRE) version to use when constructing the container image.

ktor {
   docker {
       // set a JRE version that will be used to build the container
       jreVersion.set(io.ktor.plugin.features.JreVersion.JRE_17)
   }
}

Next, we’ll configure port mappings for the image.

ktor {
   docker {
       // set a JRE version that will be used to build the container
       jreVersion.set(io.ktor.plugin.features.JreVersion.JRE_17)

       // setup port mapping for the container/service
       portMappings.set(listOf(
           io.ktor.plugin.features.DockerPortMapping(
               outsideDocker = 8080,
               insideDocker = 8080,
               io.ktor.plugin.features.DockerPortMappingProtocol.TCP
           )
       ))
   }
}

And finally, we’ll configure the external registry used when publishing the container.  In our example, we’ll configure it to use Artifact Registry.

To configure the external registry, we need to create an instance of DockerImageRegistry.  Ktor provides implementations of this interface for Docker Hub and Google Container Registry, but not Artifact Registry.

However, the interface is public, so we can create our own implementation.
The most important thing is to implement the toImage property to return a string representing the tag needed to publish our image to Artifact Registry.  That tag structure takes the following form: <region>-docker.pkg.dev/<project>/<repository>/<image name>.

class ArtifactRegistry(
   projectName: Provider<String>,
   regionName: Provider<String>,
   repositoryName: Provider<String>,
   imageName: Provider<String>,
) : DockerImageRegistry {
   override val username: Provider<String> = provider { "" }
   override val password: Provider<String> = provider { "" }
   override val toImage: Provider<String> = provider {
       val region = regionName.get()
       val project = projectName.get()
       val repository = repositoryName.get()
       val image = imageName.get()

       "$region-docker.pkg.dev/$project/$repository/$image"
   }
}

There are several key pieces of information needed for this Artifact Registry configuration:

  1. projectName should be the name of your existing GCP project in which Artifact Registry has been enabled and where you want to upload the built container
  2. regionName should be the region identifier for the GCP region in which your desired artifact repository is set up in GCP.  ex: us-west1
  3. repositoryName should be the name of the specific artifact repository where the images should be published.  ex: my-org-docker-containers
  4. imageName should be the name for a specific set of images within the repository.
  5. A reasonable default here could be to use the name of your Ktor project. You can pull this from the Gradle project using project.name

Once we have our custom implementation of DockerImageRegistry we can finish our Docker configuration.

ktor {
   docker {
       // set a JRE version that will be used to build the container
       jreVersion.set(io.ktor.plugin.features.JreVersion.JRE_17)

       // setup port mapping for the container/service
       portMappings.set(listOf(
           io.ktor.plugin.features.DockerPortMapping(
               outsideDocker = 8080,
               insideDocker = 8080,
               io.ktor.plugin.features.DockerPortMappingProtocol.TCP
           )
       ))

       // configure how the container should be deployed to Artifact Registry
       externalRegistry.set(
           ArtifactRegistry(
               projectName = provider { "<your gcp project name>" },
               regionName = provider { "<repository’s region name>" },
               repositoryName = provider { "<target repository name>" },
               imageName = provider { "<desired image name>" },
           )
        )
   }
}

Once this configuration is applied to the project, we can build our Docker image using the buildImage Gradle task.  Check out this FAQ from the jib Gradle plugin repo for more details on what an equivalent Dockerfile would look like for the generated image.

How to deploy a Ktor Docker image to Artifact Registry?

With the Ktor Docker configuration applied to the project, we can publish a Docker image for our project using the publishImage Gradle task.

This requires us to authenticate against the specified GCP project and Artifact Registry repository.

To do this, we can run the following command: gcloud auth configure-docker <region name>-docker.pkg.dev to set up gcloud as a Docker credential helper.  

Then, we just need to be sure we’ve authenticated with gcloud using gcloud auth login.

When an image with tag who’s start matches <region name>-docker.pkg.dev is pushed, it will look for this credential helper and use local gcloud authorization for the request.

Once we are authenticated, running the publishImage task should successfully publish the built image to Artifact Registry.

How to deploy a Ktor Docker image to local Docker service?

Using the Ktor Docker configuration, we can also streamline publishing of our project’s Docker image to a local Docker.

  1. Start Docker on your development machine
  2. Run the publishImageToLocalRegistry Gradle task

How to run the Ktor service container using generated Gradle tasks?

Using the Ktor Docker configuration, we can build an image and start running it with the local Docker daemon by running the runDocker Gradle task.

Conclusion

The Ktor plugin’s Docker configuration provides a streamlined workflow for developers working with Ktor services via Docker images.

By configuring the plugin to publish images to Google Cloud’s Artifact Registry, developers can publish Docker images for their service to Artifact Registry using a single Gradle task.  This task can be used by all developers, and by CI/CD workflows to provide a consistent deployment workflow.