Skip to content

Instantly share code, notes, and snippets.

@ellet0
Last active March 31, 2024 21:14
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ellet0/28e723ce3adbb3ddbd9d1ce5befe977b to your computer and use it in GitHub Desktop.
Save ellet0/28e723ce3adbb3ddbd9d1ce5befe977b to your computer and use it in GitHub Desktop.
Ktor VPS Server Deployment Steps
  1. Make sure you are on Mac OS or Linux, if your are on windows please use Git Bash instead of default terminal

And then connect to your ops server using ssh command with password or ssh key:

ssh root@SERVER_IP_ADDRESS
  1. Go to where your ktor server exists, create folder called "keys" and cd in there, then copy the absolute path of this folder then run this command:
ssh-keygen -m PEM -t rsa

and paste the following: "${theCopiedPath}/id_rsa" then enter, and enter again (no password)

create a folder in your ktor server project called "keys" or anything you prefer don't forgot to ignore it in .gitignore keys/ cut the keys to your this folder

  1. Copy the key to your server:
ssh-copy-id -i id_rsa <user>@<host>
  1. Now exit from ssh and login to your Ubuntu server via the new SSH key:
ssh -i id_rsa <user>@<host>
  1. Update dependencies: Important note: if you are on root user make sure sudo doesn't exists in the command, otherwise on distros like centos and debain will throw an error, but if you are not on root user, then please type sudo first example: "sudo apt update" and because there are commands require sudo permission (like open as admin in windows) you need to type sudo at the start of the command, if the command faield, it will tell you it need sudo permission, again you should really only use sudo if you are not on root user, apt require sudo, systemctl require sudo, the files we are editing in this tutorial also require sudo pemrission (sudo nano ...)

apt update (to update the package sources) apt upgrade -y (to update the packages) (-y is for auto accpet as yes) apt dist-upgrade (to load the new packages for the new version) apt autoremove (to remove packages that are no longer needed) apt clean (to clean the cache of the package sources)

  1. Install Java: I do prefer java 17 for now, I'm using ubuntu and the package name diffrent from package manager to another it depends on your distro

Please search more about that

In ubuntu:

apt install openjdk-17-jdk

or without the jdk (preferred):

apt install openjdk-17-jre-headless

or just (not recommend):

apt install default-jdk
  1. Add custom ssh algorithm (optional): Open /etc/ssh/sshd_config:
nano /etc/ssh/sshd_config

Put this string in there, save with Ctrl+S and exit with Ctrl+X (optional): KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group14-sha1,diffie-hellman-group-exchange-sha1,diffie-hellman-group1-sha1

and then restart the sshd service:

systemctl restart sshd
  1. Install nginx:
apt install nginx

if you want auto configurations between certbot and nginx this plugin

apt install python3-certbot-nginx

Do it manually if python3-certbot-nginx plugin doesn't work

  1. Configure nginx default configurations file: Copy the contents of nginxServerConfigurationsS and paste it into default nginx configurations using

  2. Generate ssl certificate by running this command:

for auto configurations with nginx:

certbot certonly --nginx --domain example.com --cert-name example.com

manually (which is what will we do if python3-certbot-nginx plugin don't work):

certbot certonly --manual --preferred-challenges dns --domain example.com --cert-name example.com

If you by mistake enter wrong domain, you can delete it with the following:

certbot delete --cert-name wrong.example.com

then remove the default configurations:

rm /etc/nginx/sites-available/default

create it again using:

nano /etc/nginx/sites-available/default

copy the contents of nginxServerConfigurations.txt and paste it (ctrl + v) to the nano text editor

Replace the domain with yours in server_name in both server blocks, and in ssl_certificate and ssl_certificate_key

Then save using ctrl + x and then y (yes)

  1. Restart nginx:

In ubuntu and modern linux systems use:

systemctl restart nginx.service

in old ones:

service nginx restart

Or just restart your server

  1. Create ktor server service: To automataiclly start the ktor server when the the system start You need to create a linux service

copy the contents of ktorServerService.txt ( if your java path id diffrent than mine, please change it in ExecStart, if you install openjdk-17-jdk, then you are good to go )

nano /etc/systemd/system/ktor-server.service

paste it using ctrl + v and then ctrl + x and click y (yes)

  1. Configure gradle to deploy to ktor server

Go to your build.gradle.kts in ktor server

and use build.gradle.kts as template, look at it and find it what things to remove and add, edit, or just copy the things you need sync the project

  1. Deploy the server

now go to gradle tasks and run "cleanAndDeploy" every time you made a changes note: if you have a lot of users, you might want implement service unavaliable functionallity

example: intercept(ApplicationCallPipeline.Call) { val file = File(getUserDirectory(), ".locked") if (file.exists()) { call.respondJsonText(HttpStatusCode.ServiceUnavailable, "Sorry, the api is undergoing maintenance.") return@intercept } }

so in production server whwere you have a lot of users and payments and important things before you deploy, make sure to lock the server, wait for a few mintues and then deploy, then unlock the server again

  1. Launch the service:
systemctl start ktor-server

Create a symlink to automatically launch the service on boot up:

systemctl enable ktor-server

make sure you have no error by view the log:

systemctl status ktor-server

click q to exit

  1. Others (optional): install mongodb and all other tools you need, or just use sepereated service like cloud mongodb for simpleitity (don't forgot to add the ip address of the server in network access) I might create another tutriol for that

  2. Make sure app server folder: the app folder is created by default:

the 'app' should be the same as serverFolderName variable in build.gradle.kts

cd ~/app/
ls

if not, please review build.gradle.kts and re run the deploy task if the issue still exists

you can run those commands:

cd ~
mkdir app

As I said, if you change this folder name, make sure to do the same in the build.gradle.kts

  1. Create a folder for your server files (images, verifications, assets) You might headrd of .well-known which is a folder where you put some files which required for verify that you are owning the domain and the server for example for android app links and apple site link (universal links) and it's also required for ssl certificate but I choose to use dns instead in the command

go to your routing block and add the following:

val filesFolder = "/files/".getFileFromUserDirectory()
staticFiles("/", filesFolder)
staticResources("/", "static")

you need to have those extension functions somewhere in your project:

fun getUserDirectory(): String = File(object {}.javaClass.protectionDomain.codeSource.location.toURI().path).parent fun String.getFileFromUserDirectory(): File = File(getUserDirectory(), this)

don't forgot to deploy the project again, then run this command:

mkdir -p ~/app/files/

change 'app' with the one serverFolderName in build.gradle.kts and files what the one that you choose

then create any kind of files there and I hope it works for you

  1. Use cloudflare (Highley recommened) it might need another tutorial create a account in cloudflare.com Add website Chagne nameservers Wait for some time until you cloudflare verify the change Add dns records

A record with name "@" and value with your server Ip address Cname recored with name "www" and with "@" value

Wait for few hours

and you are good to go! I might need improve this tutorial and create video for it

val ktorVersion: String by project
val kotlinVersion: String by project
val logbackVersion: String by project
val kmongoVersion: String by project
val koinVersion: String by project
val koinKtorVersion: String by project
plugins {
kotlin("jvm") version "1.8.21"
id("io.ktor.plugin") version "2.3.0"
id("org.jetbrains.kotlin.plugin.serialization") version "1.8.21"
}
group = "com.ahmedhnewa"
version = "0.0.1"
application {
// mainClass.set("io.ktor.server.netty.EngineMain")
mainClass.set("com.ahmedhnewa.ApplicationKt")
val isDevelopment: Boolean = project.ext.has("development")
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment")
// applicationDefaultJvmArgs = listOf("-Dio.ktor.development=true")
}
repositories {
mavenCentral()
google()
}
val javaVersion = JavaVersion.VERSION_17
tasks.withType<JavaCompile> {
sourceCompatibility = javaVersion.toString()
targetCompatibility = javaVersion.toString()
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.jvmTarget = javaVersion.toString()
}
val sshAntTask = configurations.create("sshAntTask")
dependencies {
implementation("io.ktor:ktor-server-core-jvm:$ktorVersion")
implementation("io.ktor:ktor-server-host-common-jvm:$ktorVersion")
implementation("io.ktor:ktor-server-call-logging-jvm:$ktorVersion")
implementation("io.ktor:ktor-server-content-negotiation-jvm:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm:$ktorVersion")
implementation("io.ktor:ktor-server-websockets-jvm:$ktorVersion")
implementation("io.ktor:ktor-server-default-headers-jvm:$ktorVersion")
implementation("io.ktor:ktor-server-cors-jvm:$ktorVersion")
implementation("io.ktor:ktor-server-compression-jvm:$ktorVersion")
implementation("io.ktor:ktor-server-caching-headers-jvm:$ktorVersion")
implementation("io.ktor:ktor-server-status-pages-jvm:$ktorVersion")
// implementation("io.ktor:ktor-server-resources:$ktorVersion")
implementation("io.ktor:ktor-server-sessions-jvm:$ktorVersion")
implementation("io.ktor:ktor-server-auth-jvm:$ktorVersion")
implementation("io.ktor:ktor-server-auth-jwt-jvm:$ktorVersion")
implementation("io.ktor:ktor-server-netty-jvm:$ktorVersion")
implementation("ch.qos.logback:logback-classic:$logbackVersion")
testImplementation("io.ktor:ktor-server-tests-jvm:$ktorVersion")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlinVersion")
implementation("io.ktor:ktor-server-rate-limit:$ktorVersion")
implementation("io.ktor:ktor-server-html-builder:$ktorVersion")
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-cio:$ktorVersion")
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-client-logging:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
implementation("io.github.cdimascio:dotenv-kotlin:6.4.1")
// implementation("com.sun.mail:javax.mail:1.6.2")
implementation("com.sun.mail:jakarta.mail:2.0.1")
implementation("org.litote.kmongo:kmongo:$kmongoVersion")
implementation("org.litote.kmongo:kmongo-coroutine:$kmongoVersion")
implementation("commons-codec:commons-codec:1.15")
implementation("io.insert-koin:koin-core:$koinVersion")
implementation("io.insert-koin:koin-ktor:$koinKtorVersion")
implementation("io.insert-koin:koin-logger-slf4j:$koinKtorVersion")
// implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.4.1")
// implementation("com.google.api-client:google-api-client:1.33.0")
// implementation("com.google.cloud:google-cloud-storage:2.4.0")
implementation("com.google.auth:google-auth-library-oauth2-http:1.3.0") // for Firebase auth
implementation("com.google.api-client:google-api-client:2.2.0") // for Google sign in
implementation("com.auth0:jwks-rsa:0.22.0")
sshAntTask("org.apache.ant:ant-jsch:1.10.13")
}
val buildingJarFileName = "temp-server.jar"
val startingJarFileName = "server.jar"
val serverUser = "root"
val serverHost = "YOUR_IP_ADDRESS"
val serverSshKey = file("keys/id_rsa")
val deleteLog = true
val lockFileName = ".serverLock"
val serviceName = "ktor-server"
val serverFolderName = "app"
ktor {
fatJar {
archiveFileName.set(buildingJarFileName)
}
}
ant.withGroovyBuilder {
"taskdef"(
"name" to "scp",
"classname" to "org.apache.tools.ant.taskdefs.optional.ssh.Scp",
"classpath" to configurations["sshAntTask"].asPath
)
"taskdef"(
"name" to "ssh",
"classname" to "org.apache.tools.ant.taskdefs.optional.ssh.SSHExec",
"classpath" to configurations["sshAntTask"].asPath
)
}
fun sudoIfNeeded(): String {
if (serverUser.trim() == "root") {
return ""
}
return "sudo "
}
fun sshCommand(command: String, knownHosts: File) = ant.withGroovyBuilder {
"ssh"(
"host" to serverHost,
"username" to serverUser,
"keyfile" to serverSshKey,
"trust" to true,
"knownhosts" to knownHosts,
"command" to command
)
}
task("cleanAndDeploy") {
dependsOn("clean", "deploy")
}
task("deploy") {
dependsOn("buildFatJar")
ant.withGroovyBuilder {
doLast {
val knownHosts = File.createTempFile("knownhosts", "txt")
try {
println("Make sure the $serverFolderName folder exists if doesn't")
sshCommand(
"mkdir -p \$HOME/$serverFolderName",
knownHosts
)
println("Lock the server requests...")
sshCommand(
"touch \$HOME/$serverFolderName/$lockFileName",
knownHosts
)
println("Deleting the previous building jar file if exists...")
sshCommand(
"rm \$HOME/$serverFolderName/$buildingJarFileName -f",
knownHosts
)
println("Uploading the new jar file...")
val file = file("build/libs/$buildingJarFileName")
"scp"(
"file" to file,
"todir" to "$serverUser@$serverHost:/\$HOME/$serverFolderName",
"keyfile" to serverSshKey,
"trust" to true,
"knownhosts" to knownHosts
)
println("Upload done, attempt to stop the current ktor server...")
sshCommand(
"${sudoIfNeeded()}systemctl stop $serviceName",
knownHosts
)
println("Server stopped, attempt to delete the current ktor server jar...")
sshCommand(
"rm \$HOME/$serverFolderName/$startingJarFileName -f",
knownHosts,
)
println("The old ktor server jar file has been deleted, now let's rename the new jar file")
sshCommand(
"mv \$HOME/$serverFolderName/$buildingJarFileName \$HOME/$serverFolderName/$startingJarFileName",
knownHosts
)
if (deleteLog) {
sshCommand(
"rm /var/log/$serviceName.log -f",
knownHosts
)
println("The $serviceName log at /var/log/$serviceName.log has been removed")
}
println("Unlock the server requests...")
sshCommand(
"rm \$HOME/$serverFolderName/$lockFileName -f",
knownHosts
)
println("Now let's start the ktor server service!")
sshCommand(
"${sudoIfNeeded()}systemctl start $serviceName",
knownHosts
)
println("Done!")
} catch (e: Exception) {
println("Error: ${e.message}")
} finally {
knownHosts.delete()
}
}
}
}
task("upgrade") {
ant.withGroovyBuilder {
doLast {
val knownHosts = File.createTempFile("knownhosts", "txt")
try {
println("Update repositories...")
sshCommand(
"${sudoIfNeeded()}apt update",
knownHosts
)
println("Update packages...")
sshCommand(
"${sudoIfNeeded()}apt upgrade -y",
knownHosts
)
println("Done")
} catch (e: Exception) {
println("Error while upgrading server packages: ${e.message}")
} finally {
knownHosts.delete()
}
}
}
}
abstract class ProjectNameTask : DefaultTask() {
@TaskAction
fun greet() = println("The project name is ${project.name}")
}
tasks.register<ProjectNameTask>("projectName")
[Unit]
Description=Ktor App Service
After=network.target
StartLimitIntervalSec=10
StartLimitBurst=5
[Service]
Type=simple
Restart=always
RestartSec=1
User=root
EnvironmentFile=/etc/environment
ExecStart=/usr/lib/jvm/java-17-openjdk-amd64/bin/java -jar /root/app/server.jar
StandardOutput=syslog
StandardError=append:/var/log/ktor-server.log
[Install]
WantedBy=multi-user.target
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket upgrade
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment