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 with this body:
{
"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"
}
]
}- 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.
- 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.
- 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.
The use case this job follows, with its automation and trigger: Hand off from your terminal.
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.