Blog Infos

If you seek to unlock the potential of a powerful trifecta — Ktor, PostgreSQL, and Docker — in your pursuit of seamless deployment, you’ve come to the right place.

Today we embark on a journey revealing the art of deploying a Ktor-PostgreSQL server using Docker on Hostinger or any other server of your choosing

Part I: Laying the Foundations — PostgreSQL and Flyway — First, we shall lay the cornerstone of our server’s infrastructure. Using Docker, we set up a PostgreSQL database. To ensure seamless migrations, we shall use the help of Flyway.

Part II: Launching the Ktor Server — Docker at its Finest — Next we will look at deploying our code on a server using Docker and Docker Hub.

Part III: Reaching Zenith — Seamlessly Updating and Migrating — Finally we will learn the art of server updates and database migrations.

Part I: Laying the Foundations — PostgreSQL and Flyway

In this article we won’t delve into the intricate details of Docker, Flyway, PostgreSQL or Ktor — there’s already a wealth of knowledge on each. Instead, we focus on the art of combining these powerful technologies together.

If you haven’t already, install docker following these instructions:

We begin by containerising our ktor project and creating a Dockerfile in the root of our ktor project directory. This docker configuration is for our local machine, we’ll look at docker configuration for the server when we setup our server.

This will build our project and generate a .jar file through the buildFatJar Gradle task.

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

Next, We need to create a docker-compose.yml file in the root of our project with two services one for the database and the other for ktor server.

For the database service, we’ll be utilizing PostgreSQL, and thus, the appropriate image to be used is postgres. It is worth noting that if you are working on an Apple M1 machine, you must specify the platform as linux/amd64 to ensure compatibility. However, if your server does not utilize the M1 chip, this specification won’t be necessary. Next, we also need to provide a name for our database container, along with defining volumes, ports, and a health check. We pass sensitive information, such as the database name, username, and password, securely using a .env file.

    image: postgres
    platform: linux/amd64
    restart: always
    container_name: backend-db
      - pg-volume:/var/lib/postgresql/data
      - postgres.env
      - "5432:5432"
      test: [ "CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" ]
      interval: 5s
    build: .
    platform: linux/amd64
    container_name: backend-ktor
    restart: always
      - "8080:8080"
      - postgres.env
        condition: service_healthy
    pg-volume: {}

For the ktor service, we will build the ktor project and specify the configuration, such as linux/amd64 architecture, container name, ports. Finally, we pass the same .env file to our ktor project as used in database service for it to connect to the database and perform read/write operations.

Job Offers

Job Offers

    Information Security Engineer

    London, UK
    • Full Time
    apply now


, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...


To pass the database name, user, and password, we create a .env file in the root directory of the project. Which looks like this:


Obviously, you have the flexibility to add more values to the .env file, including ports, drivers, and other configurations, as needed. We then read the values in our ktor project using the application.conf file as such:

ktor {
    deployment {
        port = 8080
        port = ${?PORT}
    application {
        modules = [ com.way.ApplicationKt.module ]

storage {
    driverClassName = "org.postgresql.Driver"
    jdbcURL = "jdbc:postgresql://db:5432/"${POSTGRES_DB}
    user = ${POSTGRES_USER}
    password = ${POSTGRES_PASSWORD}

That concludes the setup for Docker on our local machine. Next, we look at database migrations.

In line with Duncan McGregor’s wisdom — “Nobody ever got fired for using Flyway,” we opt for Flyway as our trusted tool for handling database migrations.

Let’s start by adding flyway to our ktor project. You can also follow this quick start documentation from Flyway:


To create our database tables, we use Flyway instead of Exposed or H2. We do this by creating a migration file named V1__Create_database_table.sql This file is placed under the path src/main/resources/db/migration which enables Flyway to recognize and execute the necessary database changes.

create table if not exists public."user"
    email          varchar(128),
    first_name     varchar(64)  not null,
    last_name      varchar(64)  not null,
    phone_number   bigint       not null
    primary key,
    date_of_birth  date,
    date_of_signup date         not null,
    password       varchar(256) not null,
    salt           varchar(256) not null

alter table public."user"
    owner to {DATABASE_USER_NAME};

The next step is establishing a connection between Flyway and our database by retrieving the required values from the application.conf file and invoking the migrate() function, which will identify the latest migration file and implementing the necessary alterations in our database. Typically, the initial version of the migration file is dedicated to creating the tables in the database. We can then connect to our database using either exposed or any other ORM and perform db operations.

object DatabaseFactory {
    fun init(config: ApplicationConfig) {
        val driverClassName ="storage.driverClassName").getString()
        val jdbcURL ="storage.jdbcURL").getString()
        val dbUser ="storage.user").getString()
        val dbPassword ="storage.password").getString()

        val flyway = Flyway.configure().dataSource(jdbcURL, dbUser, dbPassword).load()

        Database.connect(url = jdbcURL, user = dbUser, password = dbPassword, driver = driverClassName)

    suspend fun <T> dbQuery(block: suspend () -> T): T =
        newSuspendedTransaction(Dispatchers.IO) { block() }

At this point, we are all set to run the docker compose up command in our project directory. This will trigger the download and setup of the PostgreSQL image, build our Ktor project, and seamlessly create the necessary database tables through Flyway. By executing this single command, we can effortlessly orchestrate the entire process, ensuring that our application environment is up and running smoothly with the database fully configured and ready for use in our local machine.

#kotlinmultiplatform #ktor #kotlin

This article was previously published on



This tutorial is the second part of the series. It’ll be focussed on developing…
A few weeks ago I started with a simple question — how to work…
Ktor is an asynchronous framework used in development of microservices and web applications. Kotlin…
In this article, we explore the step-by-step process of configuring our server, as well…

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.