[ Home ]

# Deploying Ktor app in a VPS using github actions

## Overview

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.

A rocketship in space.

## Setting up the project

### Ktor Gradle Plugin

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")
    }
}

### Dockerfile

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

### Github Action Setup

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=

## Final notes