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.
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.
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.
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).
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.
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.
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).
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 :
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 coniguration and set the start command to
./gradlew start
I've chosen the latter for simplicity this time.
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.
That's it, this time we're in business! If we go to the generated URL, our application answers as expected
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.
And just like expected, you will also have access to information about the deployment directly in your Pull Request:
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!