The deployable content host for readingbat.com. This
repo is intentionally thin: the web server, routing, persistence, and challenge
runtime all live in readingbat-core,
which is consumed here as a binary dependency. What lives in this repo is the
content composition and the deployment plumbing around it.
- Top-level
Content.kt— composes the full site - Java and Kotlin content
- Python content
- JDK 25 (the Gradle toolchain will resolve one via foojay if you don't already have it)
- Docker +
buildx(only needed for releases)
All other versions — Gradle, Kotlin, Ktor, plugins — are pinned in
gradle/libs.versions.toml.
./gradlew run # run the app locally
./gradlew build -x test # build without tests
./gradlew buildFatJar # produce build/libs/server.jar
java -jar build/libs/server.jarThe app listens on 8080 (HTTP) by default, with additional ports for JMX and
metrics — see the Dockerfile for the full list.
./gradlew check # full verification (tests + lint)
./gradlew test # tests onlyTests use Kotest StringSpec. Test sources live under
src/test/kotlin/com/readingbat/.
.github/workflows/ci.yml runs on every push and pull
request against master. It builds, tests, and lints on JDK 25 (Temurin) with a
single ./gradlew build — detekt and lintKotlin both attach to check, so
that one command is the same verification you get locally.
./gradlew lintKotlin detekt # static analysis + style check
./gradlew formatKotlin # apply ktlint fixesProject formatting rules are in .editorconfig. Line endings are
normalized by .gitattributes: text files are stored and checked
out with LF (*.bat is the sole CRLF exception), so gradlew and the machines/
shell scripts stay runnable on Linux and in Docker regardless of your
core.autocrlf setting.
Most common workflows have a make target. Run make help for the full list:
make build # clean and build (no tests)
make tests # full test suite
make lint # lintKotlin + detekt
make format # apply formatting
make uberjar # build the fat jar
make run-uber # run the fat jar
make release # build + push multi-arch Docker image
make deploy # deploy via secrets/deploy-app.sh
make upgrade-wrapper # bump the Gradle wrapper to the catalog-pinned versionThe image is built from eclipse-temurin:25-jdk-alpine, runs as a non-root
readingbat user, and declares a HEALTHCHECK on /ping. make release
publishes a multi-arch (linux/amd64,linux/arm64) image to
pambrose/readingbat:{latest,$VERSION}.
A multi-instance local composition is provided in
docker-compose.yml.
The image sets JAVA_TOOL_OPTIONS to write a heap dump on OutOfMemoryError, into
/app/dumps, and each composed service gets its own named volume there. Two reasons it
is a named volume rather than a bind mount: Docker creates a missing bind-mount directory as
root while the container runs as uid 1000, so the JVM could not write the dump, whereas a
named volume is initialized from the image's own /app/dumps and inherits its readingbat
ownership. And one volume per service, because each container is pid 1 in its own namespace —
a shared volume would have all three writing java_pid1.hprof over each other.
Retrieve a dump with:
docker run --rm -v readingbat-site_readingbat0-dumps:/dumps -v "$PWD":/out \
alpine cp /dumps/java_pid1.hprof /out/Two things to know. A dump is roughly the size of the live heap, and MaxRAMPercentage=75
allows that to be most of the machine's RAM — keep an eye on disk. And a dump is a copy of
live memory: it contains session secrets, database credentials and user data, so treat the
file itself as a secret. Setting JAVA_TOOL_OPTIONS in docker_env_vars overrides the image
default, since env_file wins over ENV.
- Bump
version=ingradle.properties. - Bump the pinned image tags in
docker-compose.ymlandmachines/content/run.shto the new version. - Move
[Unreleased]entries inCHANGELOG.mdunder the new version and add a highlights entry toRELEASE_NOTES.md. make releaseto build and push the Docker image.- Follow the runbook in
docs/release_notes.mdto roll the deployment on Digital Ocean. - Cut a GitHub release: tag
X.Y.Z(novprefix), titlevX.Y.Z.
See CLAUDE.md for repo conventions (version-catalog discipline,
configuration-cache safety, shadowJar duplicate handling, and the gotchas that
the code alone won't teach you) — useful for both humans and AI coding agents.
Apache License 2.0 — see LICENSE.txt.