CI/CD for a Kotlin + Ktor + Postgres App with GitHub Actions
Build with Gradle and run Ktor tests against a real Postgres database on every push.
This recipe tests a Kotlin Ktor service that talks to Postgres through Exposed or jOOQ. CI runs the Gradle build against a Postgres service container, then builds a fat jar for deployment.
What the pipeline does
- set up the JDK with Gradle cache
- start a Postgres service container
- run the Gradle test task
- build the fat jar with the shadow plugin
- upload the jar artifact
The workflow
A Postgres service backs the data layer in tests. The Gradle wrapper runs check, then shadowJar packages a standalone runnable jar.
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: ktor
ports: ["5432:5432"]
options: >-
--health-cmd "pg_isready" --health-interval 10s
--health-timeout 5s --health-retries 5
env:
DATABASE_URL: jdbc:postgresql://localhost:5432/ktor
DATABASE_USER: postgres
DATABASE_PASSWORD: postgres
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
- uses: gradle/actions/setup-gradle@v4
- run: ./gradlew check
- run: ./gradlew shadowJar
- uses: actions/upload-artifact@v4
with:
name: ktor-jar
path: build/libs/*-all.jarCaching and speed
gradle/actions/setup-gradle restores the Gradle dependency and build cache and enables the build cache. Kotlin compilation plus Postgres integration tests are slow; cheaper managed runners such as Latchkey (around 69% cheaper than GitHub-hosted) keep the run fast and auto-retry a flaky Postgres image pull.
Deploying
The -all.jar from shadowJar is self-contained. Build a Docker image around it (FROM eclipse-temurin:21-jre) in a deploy job that needs the build job, then push and roll out once check passes on main.
Key takeaways
- Back Ktor data-layer tests with a Postgres service container.
- Use gradle/actions/setup-gradle for dependency and build caching.
- Build a self-contained fat jar with the shadow plugin for deploy.