This is my simple docker + VPS setup for deploying Ktor application. Read ktor’s deployment documentation if you want to learn about different/right way of deploying a Ktor app.
In this setup we deploy automatically whenever a code change is pushed to GitHub
. We rely on github actions
to build and push the docker image to ghcr
, and to trigger a new deployment on the server.
Configure ktor gradle plugin
, so we can build a fatJar
using gradlew fatJar
command.
plugins {
id("io.ktor.plugin") version "3.0.1"
}
application {
// https://ktor.io/docs/server-dependencies.html#create-entry-point
mainClass.set("io.ktor.server.netty.EngineMain")
}
ktor {
fatJar {
// set the fatJar name here
archiveFileName.set("app.jar")
}
}
We will use a simple Dockerfile
. This image
requires us to build the fatJar
externally before building our docker image.
FROM amazoncorretto:17
COPY build/libs/app.jar /app/app.jar
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
Create a Docker Compose configuration that includes our application and its database
.
services:
ktor:
container_name: ktor_app
environment:
DB_NAME: ${DB_NAME}
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}
image: ghcr.io/<username>/ktor-sample:latest
ports:
- "9080:9080"
networks:
- backend_network
depends_on:
- db
db:
image: postgres:16
container_name: postgres_db
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -d ${DB_NAME} -u ${DB_USER}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
restart: unless-stopped
networks:
- backend_network
volumes:
postgres_data:
networks:
backend_network:
driver: bridge
Your directory should look like this after these steps
.
├── Dockerfile
└── compose.yaml
Create a github workflow that deploys whenever new changes are merged into our main
branch.
name: Build and deploy to VPS
on:
push:
branches:
- main
permissions:
packages: write
contents: read
env:
IMAGE_NAME: ktor-app
jobs:
build-and-deploy:
runs-on: ubuntu-22.04
steps:
- name: Check out repository code
uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'oracle'
java-version: '17'
cache: 'gradle'
cache-dependency-path: |
**/.gradle*
**/gradle-wrapper.properties
- run:
./gradlew build
- name: Build image
run: docker build . --file Dockerfile --tag $IMAGE_NAME
- name: Log in to registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Push image
run: |
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
# This changes all uppercase characters to lowercase.
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
docker tag $IMAGE_NAME $IMAGE_ID
docker push $IMAGE_ID
- name: Create .env file
run: |
echo "DB_USER=${{ secrets.DB_USER }}" >> .env
echo "DB_PASSWORD=${{ secrets.DB_PASSWORD }}" >> .env
echo "DB_NAME=${{ secrets.DB_NAME }}" >> .env
- name: copy file via ssh key
uses: appleboy/scp-action@master
with:
host: ${{ vars.VPS_IP }}
username: ${{ vars.VPS_USERNAME}}
key: ${{ secrets.SSH_PRIVATE_KEY }}
port: 22
source: "compose.yaml,.env"
target: ktor-sample
- name: Set up SSH Key and Deploy my App on Server
uses: appleboy/ssh-action@master
with:
host: ${{ vars.VPS_IP }}
username: ${{ vars.VPS_USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
port: 22
script: |
cd ~/ktor-sample
docker compose pull
docker compose --env-file .env up -d
Secrets required by the github workflow:
# private key required by github action to ssh into the VPS
SSH_PRIVATE_KEY=
DB_NAME=
DB_PASSWORD=
DB_USER=
Variables required by the workflow:
VPS_IP=
VPS_USERNAME=
ghcr
.jdbc
url in our application should look like: "jdbc:postgresql://db:5432/$dbName"
i.e. should use db
as a hostname rather than localhost
.