# Dockerize a Ktor Server

Yes. An agent reads your Ktor build, writes a multi-stage Dockerfile around the fat jar, adds the app to Compose next to Postgres, and proves it answers.

[Run this on Rush](https://cloud.getrush.ai/run/dockerize-app/ktor/), in a Project that has your repository.

## What the agent does

The agent clones the server into its own cloud sandbox and reads the build before it writes a Dockerfile. In a service like the `postgres` sample in ktorio/ktor-samples, `build.gradle.kts` applies `kotlin("jvm")` 2.4.20, `io.ktor.plugin` 3.6.0 and the serialization plugin, sets `mainClass` to `io.ktor.server.netty.EngineMain`, pins `jvmToolchain(17)`, and depends on `ktor-server-core-jvm`, `ktor-server-content-negotiation-jvm`, `ktor-serialization-kotlinx-json-jvm`, `ktor-server-netty-jvm`, `ktor-server-config-yaml`, Logback 1.5.21 from `gradle.properties` and `org.postgresql:postgresql` 42.7.8. The Gradle wrapper is 9.2.1. `src/main/resources/application.yaml` names the module `com.example.PostgresApplicationKt.module` and port 8080. There is no Dockerfile in the sample or anywhere in the repository, no `.dockerignore`, and no test source set; `README.md` says to run the Gradle task `databaseInstance`, which shells out to `docker-compose up`, and then `./gradlew run`.

The database connection is the part a container breaks. `src/main/kotlin/com/example/plugins/Routing.kt` has `connectToPostgres`, which reads `POSTGRES_DB`, `POSTGRES_USER` and `POSTGRES_PASSWORD` from `postgres.yaml` through `YamlConfig` and then builds the URL as `jdbc:postgresql://localhost:5432/` plus the database name. Inside a container `localhost` is the container, so the agent changes that one line to read a `POSTGRES_HOST` environment variable with `localhost` as the default, leaving `./gradlew run` on a laptop unchanged. `docker-compose.yaml` already defines `db` as `postgres:18.0-alpine` with a named volume, port 5432 and the credentials from `postgres.yaml`; the agent adds an `app` service built from the new Dockerfile, with `POSTGRES_HOST=db`, port 8080, and `depends_on` with a `pg_isready` health check on `db`, because `ArticleService` runs `CREATE TABLE IF NOT EXISTS ARTICLES` in its constructor and fails if Postgres is not accepting connections yet.

The Dockerfile is two stages. The build stage starts from a JDK 17 image to match the toolchain, copies the wrapper, `settings.gradle.kts`, `build.gradle.kts` and `gradle.properties` first so dependency resolution caches as a layer, then copies `src/` and runs `./gradlew buildFatJar --no-daemon`, the task the Ktor Gradle plugin provides, which writes `build/libs/ktor-postgres-all.jar` from the `rootProject.name` in `settings.gradle.kts`. The runtime stage is a JRE 17 image with a non-root user, the jar copied in, `EXPOSE 8080` and an `ENTRYPOINT` of `java -jar`. A `.dockerignore` keeps `build/`, `.gradle/` and the IDE directories out of the context. The plugin also offers `buildImage` and `runDocker` tasks that produce an image without a Dockerfile; the agent mentions them in the pull request and keeps the Dockerfile, because Compose, CI and a registry push all read one.

Then it proves the image. `docker build` must finish from a clean context; `docker compose up --build` must bring `db` healthy and `app` listening; `curl -X POST localhost:8080/articles` with a JSON `title` and `body` must answer `201` with the new id, `GET /articles/{id}` must return it, and `GET /articles/0` must answer `404`, the codes `Routing.kt` sends. The agent also runs `./gradlew build` outside Docker so the one-line change to `connectToPostgres` compiles the way the repository's `build-all.sh` would run it in CI on JDK 24.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Dockerize this Ktor server. Read build.gradle.kts, settings.gradle.kts, gradle.properties, gradle/wrapper/gradle-wrapper.properties, src/main/resources/application.yaml, src/main/kotlin/com/example/plugins/Routing.kt, docker-compose.yaml and postgres.yaml. Write a multi-stage Dockerfile that builds the fat jar with ./gradlew buildFatJar on a JDK matching jvmToolchain and runs it on a JRE image as a non-root user on port 8080, with a .dockerignore for build and .gradle. Make the Postgres host configurable through a POSTGRES_HOST environment variable with localhost as the default, add an app service to docker-compose.yaml that builds the image, sets POSTGRES_HOST=db and waits on a pg_isready health check for db. Run docker build and docker compose up --build in the sandbox, create an article with POST /articles, read it back with GET /articles/{id} and confirm GET on a missing id returns 404, run ./gradlew build, and open a pull request with the image size, the build log tail and the curl output in its description.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is a pull request on its own branch adding `Dockerfile` and `.dockerignore`, changing `docker-compose.yaml` to add the `app` service and the health check, and changing one line in `src/main/kotlin/com/example/plugins/Routing.kt` to read `POSTGRES_HOST`. The description holds the last lines of the `docker build` output with the image size, the Compose log where `db` turns healthy and `app` logs its port, and the three `curl` calls with their status codes and bodies.

The transcript at `GET /o/{org}/sessions/{id}/messages` keeps the full build log, the Compose output and the `./gradlew build` run, so a reviewer can see how long dependency resolution took and which layer the source copy landed in. `GET /o/{org}/p/{project}/sessions/{id}/changes` shows each file's diff.

Before merging, a reviewer checks the base images against the JDK the team ships, confirms the health check and `POSTGRES_HOST` default keep `./gradlew run` working on a laptop, and decides whether the image should be pushed by CI. A follow-up on the same session can add a `HEALTHCHECK` to the Dockerfile or switch to the plugin's `buildImage` task if the team prefers no Dockerfile.

## Run it on every Ktor service without a Dockerfile

Run it once from the terminal, then let a script repeat it. A loop on your server lists repositories with a `build.gradle.kts` that applies `io.ktor.plugin` and no `Dockerfile`, sends one `POST /o/{org}/sessions` per repository with this body and that repository in `resources`, and uses the repository name as the `request_id` so a rerun returns the run already started. Each run gets its own sandbox and its own Docker daemon, so builds do not share a cache or a port.

Give the key a webhook URL and Rush POSTs `run.completed` with `data.session_url` when each turn ends. A handler reads the session, takes the pull request from it and files the image size beside the service name; a run that ended `needs_review` because the Compose stack never turned healthy is listed for a person to read.

## Why run it on Rush?

A Dockerfile that builds is not a Dockerfile that runs: the jar name comes from `settings.gradle.kts`, the JDK has to match `jvmToolchain`, and the database host that works on a laptop is wrong in a container. The agent builds the image in its own sandbox, starts it beside the real Postgres from `docker-compose.yaml`, and sends the requests `Routing.kt` handles, so the pull request shows an app answering on 8080 rather than a Dockerfile that looks right.

You start it from a terminal and walk away. The sandbox parks when the turn ends and wakes for a follow-up, so adding a `HEALTHCHECK`, trimming the image or wiring a registry push is the next message on the same session, with the build cache still warm.

## Frequently asked questions

### Why change Routing.kt at all?

Because `connectToPostgres` hard-codes `localhost` in the JDBC URL while reading the database name and credentials from `postgres.yaml`. In Compose the database is reachable as `db`, so the host becomes an environment variable with `localhost` as its default. Nothing else in the server changes, and `./gradlew run` on a laptop behaves as before.

### Why a Dockerfile rather than the Ktor plugin's buildImage task?

The plugin's `buildImage`, `runDocker` and `publishImage` tasks do produce an image from the fat jar, and the pull request names them. A Dockerfile is what `docker compose build`, a CI job and a registry all read without Gradle, so the agent writes one and uses `buildFatJar` inside it. Switching is a follow-up if the team prefers the task.

### Why does the app service wait on a health check?

`ArticleService` creates the `ARTICLES` table in its constructor, which runs when the module starts, so the app crashes if Postgres is still starting. A `pg_isready` health check on `db` and `depends_on` with `condition: service_healthy` make Compose start the app only when the database accepts connections.

### Does it run the tests?

The sample has no test source set, so there are none to run; the agent runs `./gradlew build` to compile the change and the request sequence against the running container as the functional check, and the pull request says so. Writing tests on `ktor-server-test-host` is a separate job.
