Hosting Kotlin applications using Coolify

TL;DR : With Coolify you can host you Kotlin applications in seconds on your own server and benefit from auto deploys, custom domains, preview branches and more. You can see the code here, and access the sample here.

Hosting Kotlin applications using Coolify
The Coolify and the Ktor logos with a heart in between them

TL;DR : With Coolify you can host you Kotlin applications in seconds on your own server and benefit from auto deploys, custom domains, preview branches and more. You can see the code here, and access the sample here.

Lately, I've been increasingly thinking about the fact that all of my applications / experiments are spread across providers (Supabase, AWS, Koyeb, Digital Ocean, ...) and I've been toying with the idea of owning all of this back on my own servers. After discovering Hetzner auction servers, I realised that I could have a super beefy server for very cheap and decided to try it out.

Installing Coolify is as simple as running $ curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash on your server.

A sample Kotlin application

For this test, I'll go to the Ktor Starter website and create the simplest application I can think of.

My minimal ktor application configuration

I'll then unzip the repo and create a GitHub repository from it (using the GitHub CLI, get it if you don't have it yet it's awesome 😊).

$ unzip ktor-sample-coolify.zip -d ktor-sample-coolify
$ cd ktor-sample-coolify
$ gh repo create . 
## Some setup, and final repository push to GitHub

Once that is done, I can access my repository here.

Creating a Coolify GitHub application

We have to deploy this application to Coolify now. There are several ways to do it, but the most powerful one will be via a GitHub app, we'll see why very soon.

To do this, we'll add a new GitHub app source to Coolify.

The GitHub app dialog

It will then ask us which features we want to activate, and we'll be redirected to GitHub to approve the creation of the app, and then requested to select which repositories to apply this application to (I selected them all, but you can also choose to segregate better and only add the one repository we created earlier).

Registering a new GitHub app

Deploying our Ktor application

Now that the connection between Coolify and GitHub is setup, we want to deploy our Ktor application. To do this, we create a new resource and select the Private repository with GitHub option.

Menu for creating a new resource

I'm not going to show you all of the dialogs, but you'll need to select which server to deploy on, which GitHub app to use and then which repository to choose.

Once all of this is done, we'll have access to our deployment configuration. We'll select the main branch for the deployment, and the port 8080 which is the default Ktor port.

Basic configuration for our Ktor deployment

Once that is done, we can hit the deploy button. By the way, we can also very much appreciate the fact that Coolify will use sslip.io to generate a domain URL for your app without you having to setup anything (Granted, it's not the URL we want but it's so much better than an IP address and port combination).

Sslip domain generated for us by Coolify

First roadblock : Invalid Nixpack start command

Once thing that I haven't mentioned yet here is that our Ktor sample application does not have any kind of DockerFile. Nixpacks will magically detect which kind of project it is yet, and start building it, running gradle tests, building the project and creating a Docker deployment based on its own inference. I didn't know about this yet, and honestly I was 🀯.

The issue though, is that our deployment fails :

No main manifest attribute found error

Now, that's a very well known error for any seasoned JVM developer I think 😊. We can spot the issue rather quickly when investigating the logs. Here is the commands that Nixpack will use to build/deploy our project :

╔══════════════════════════════ Nixpacks v1.24.1 ═════════════════════════════╗
β•‘ setup β”‚ jdk17, gradle, curl, wget β•‘
║─────────────────────────────────────────────────────────────────────────────║
β•‘ build β”‚ ./gradlew clean build -x check -x test β•‘
║─────────────────────────────────────────────────────────────────────────────║
β•‘ start β”‚ java $JAVA_OPTS -jar $(ls -1 build/libs/*jar | grep -v plain) β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

The issue, however, is that we actually build 2 jars during our build step, and Nixpack runs the incorrect one in its start phase. This is not a Ktor only issue by the way, it seems to happen for Spring boot too.

 ο…Ή β•±  ~/Dev/ktor-sample-coolify/b/libs β•± ο„“  feat/automat…-branch-test ξ‚° ls -la                                                                                         ξ‚² βœ” β•± 12:00:46 ο€—
total 30584
drwxr-xr-x   7 julienlengrand-lambert  staff       224 Jun 10 15:23 .
drwxr-xr-x  11 julienlengrand-lambert  staff       352 Jun 10 15:22 ..
-rw-r--r--@  1 julienlengrand-lambert  staff      6148 Jun 10 15:23 .DS_Store
drwx------   7 julienlengrand-lambert  staff       224 Jun 10 15:23 quest.lengrand.ktor-sample-coolify-0.0.1
-rw-r--r--@  1 julienlengrand-lambert  staff      5193 Jun 10 15:21 quest.lengrand.ktor-sample-coolify-0.0.1.jar
drwx------  18 julienlengrand-lambert  staff       576 Jun 10 15:23 quest.lengrand.ktor-sample-coolify-all
-rw-r--r--@  1 julienlengrand-lambert  staff  15638896 Jun 10 15:22 quest.lengrand.ktor-sample-coolify-all.jar

We have 2 ways to fix this :

  • Create a nixpacks.toml to customize the start command
  • Change the Coolify configuration and set the start command to ./gradlew start

I've chosen the latter for simplicity this time.

Custom start command in Coolify

We change, save and press deploy again.

Second roadblock : Issue with healthchecks

Deployment somehow fails again. This time, it seems to be due to the automated healthchecks from Coolify to indicate that the application is unhealthy. And the default behaviour for Coolify is to 404 any traffic to unheathly applications.

[COMMAND] docker inspect --format='{{json .State.Health.Status}}' xc8g0s0-100357313316
[OUTPUT]
"unhealthy"

[2024-Jun-11 10:06:05.645616]

[COMMAND] docker inspect --format='{{json .State.Health.Log}}' xc8g0s0-100357313316
[OUTPUT]
[{"Start":"2024-06-11T12:05:43.755413473+02:00","End":"2024-06-11T12:05:43.808713519+02:00","ExitCode":1,"Output":""},{"Start":"2024-06-11T12:05:48.809795855+02:00","End":"2024-06-11T12:05:48.8473437+02:00","ExitCode":1,"Output":""},{"Start":"2024-06-11T12:05:53.848150166+02:00","End":"2024-06-11T12:05:53.985646744+02:00","ExitCode":1,"Output":""},{"Start":"2024-06-11T12:05:58.986804475+02:00","End":"2024-06-11T12:05:59.036324085+02:00","ExitCode":1,"Output":""},{"Start":"2024-06-11T12:06:04.037004721+02:00","End":"2024-06-11T12:06:04.07944818+02:00","ExitCode":1,"Output":""}]

[2024-Jun-11 10:06:05.648560] Attempt 10 of 10 | Healthcheck status: "unhealthy"
[2024-Jun-11 10:06:05.651092] Healthcheck logs: (no logs) | Return code: 1
[2024-Jun-11 10:06:05.653988] ----------------------------------------
[2024-Jun-11 10:06:05.656408] Container logs:
[2024-Jun-11 10:06:05.745223] Downloading https://services.gradle.org/distributions/gradle-8.4-bin.zip
............10%............20%.............30%............40%.............50%............60%.............70%............80%.............90%............100%

Welcome to Gradle 8.4!

Here are the highlights of this release:
- Compiling and testing with Java 21
- Faster Java compilation on Windows
- Role focused dependency configurations creation

For more details see https://docs.gradle.org/8.4/release-notes.html

Starting a Gradle Daemon (subsequent builds will be faster)
[2024-Jun-11 10:06:05.748707] ----------------------------------------
[2024-Jun-11 10:06:05.751778] Removing old containers.
[2024-Jun-11 10:06:05.754548] New container is not healthy, rolling back to the old container.
[2024-Jun-11 10:06:06.335355] Rolling update completed.

I haven't found the solution for this just yet, so I've decided to disable healthchecks for now. We press deploy again.

Disabling healthchecks

That's it, this time we're in business! If we go to the generated URL, our application answers as expected

Hello world!

Where the magic begins!

Now that our configuration is valid, we can benefit from all the magic that a Fly.io, Koyeb or any other cloud provider can offer us, but on our own terms!

Preview Pull requests

One of the features I love the most is preview pull requests. We create a new branch with a custom endpoint:

fun Application.configureRouting() {
    routing {
      ...
        get("/mood/{mood}"){
            call.respondText("Are you feeling ${call.parameters["mood"]}?")
        }
    }
}

We commit, push the new branch and create a Pull Request. Coolify will automatically detect this, start a new deployment and generate a new SSlip URL, all of this while our main deployment is still running! You can see this happen here.

Deployment after a new Pull Request is created
Dedicated URL for the Pull Request, in parallel to the main deployment

And just like expected, you will also have access to information about the deployment directly in your Pull Request:

Coolify keeping me updated about the deployment straight from my PR

Similarly, any merge / commit to main trigger a new deployment, so you can basically have CI/CD with a great Developer Experience, all of that on your own premises.

There you go, I'm feeling happy. 😊

Custom secure domains

This is not the object of today's article, but adding custom domains to Coolify is also very simple and the tool will take care of all the SSL certificates setup / renewal for you automagically so it takes seconds to create shareable domains, including multiple wildcards. For example, you can access my app here, here and here too and all I had to do was change the "Domains" input and restart.

A word of conclusion

You know me by now, I'm a sucker for good DevEx. And I usually love to share my excitement for new tooling, which is why I'm a big fan of tools like Supabase, TinyBird, Koyeb or Digital Ocean.

What impresses me A LOT here though, is that all of this is available for free locally as well, and is mainly developed by a single person. I honestly wish the very best to Andras and will definitely be supporting his work further.

I could deploy a complete application within an hour using lots of tooling I have no experience about, and without having to read any documentation. That allows me to just focus on writing my application, and I just love this.

Now, there's still a lot I want to explore. Obviously, we need to fix those healthchecks. I also want to create a more "production like" application, with a database, observability setup and more, but we'll see this soon, I have total confidence I can figure it out! 😊

Try out Coolify, it's worth it!