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: https://docs.docker.com/engine/install/
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.
services: db: image: postgres platform: linux/amd64 restart: always container_name: backend-db volumes: - pg-volume:/var/lib/postgresql/data env_file: - postgres.env ports: - "5432:5432" healthcheck: test: [ "CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" ] interval: 5s ktor: build: . platform: linux/amd64 container_name: backend-ktor restart: always ports: - "8080:8080" env_file: - postgres.env depends_on: db: condition: service_healthy volumes: 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
To pass the database name, user, and password, we create a .env file in the root directory of the project. Which looks like this:
POSTGRES_DB={DATABASE_NAME} POSTGRES_USER={DATABASE_USER_NAME} POSTGRES_PASSWORD={DATABASE_PASSWORD}
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: https://documentation.red-gate.com/fd/quickstart-api-184127575.html
implementation("org.flywaydb:flyway-core:9.20.1")
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 = config.property("storage.driverClassName").getString() val jdbcURL = config.property("storage.jdbcURL").getString() val dbUser = config.property("storage.user").getString() val dbPassword = config.property("storage.password").getString() val flyway = Flyway.configure().dataSource(jdbcURL, dbUser, dbPassword).load() flyway.migrate() 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 proandroiddev.com