Running a Java or Kotlin Discord Bot (JDA) on a Small Container

·5 min read·Updated
javakotlinjdadiscordmemoryhow-to

A JDA or Discord4J bot runs perfectly well on a small container, with two things to get right that other stacks don't have. First, deploy it with a Dockerfile — the zip flow's build agent detects Python, Node.js, and prebuilt .NET, and Java isn't among them. Second, tell the JVM how much memory it may use, because its default heap fraction is deliberately conservative and its non-heap overhead — metaspace, thread stacks, JIT code cache — is what actually surprises people. Java is heavier than Python or Go here, so be realistic: this is a Hobby-preset workload, not a bottom-of-the-ladder one.

Deploy with a Dockerfile

The zip upload path infers a runtime from your source and handles Python, Node.js, and prebuilt .NET publish output. Java isn't one of them, so a JDA project uploaded as a zip won't be detected.

That's fine, because a JVM build is well-served by a multi-stage Dockerfile anyway:

FROM eclipse-temurin:21-jdk AS build
WORKDIR /src
COPY . .
RUN ./gradlew --no-daemon shadowJar

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /src/build/libs/*-all.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

Push it by git and the platform builds it. Note the runtime stage uses a JRE image, not the JDK — you don't need a compiler to run a jar, and it meaningfully shrinks the image.

Your build may not fit

This bites Java harder than most stacks, so check it first: the build runs in its own container with a smaller memory limit than your app, around 0.75 GB. Gradle and Maven are memory-hungry, and a Gradle daemon on a large project will happily ask for more than that.

Mitigations, in order:

  • --no-daemon, as above. The daemon exists to speed up repeat builds on a developer machine; in a one-shot container build it's overhead.
  • Cap the build JVM: GRADLE_OPTS="-Xmx512m" or MAVEN_OPTS="-Xmx512m".
  • Build in CI and push the image instead. That sidesteps the build container entirely and is the reliable answer for a large project — more on the build limit.

A build that stops partway through a compile step with no error is the signature. It rarely announces itself as an out-of-memory failure.

The JVM already knows it's in a container

Good news, and it's official: container detection is on by default. Oracle's documentation states that the VM provides automatic container detection so it can determine the memory and processors available to it, and that "the default for this flag is true, and container support is enabled by default" (Oracle java tool reference).

So the JVM reads your app's memory allocation rather than the host's. You don't need -XX:+UseContainerSupport; it's already on.

If you ever want to confirm what it detected, that same documentation points at -Xlog:os+container=trace, which prints exactly what the JVM thinks its limits are. That's a two-minute check worth doing once.

Set the heap fraction explicitly

Container-aware doesn't mean well-sized. By default the JVM claims only a fraction of the container's memory for the heap — commonly reported as 25% (DZone) — which is deliberately cautious and usually not what you want on a container you sized for exactly one application.

Set it yourself:

ENTRYPOINT ["java", "-XX:MaxRAMPercentage=70", "-jar", "app.jar"]

Don't set it to 90. The heap is not the JVM's whole footprint. Metaspace, thread stacks, the JIT code cache, GC bookkeeping and native buffers all live outside it, and they're a meaningful share on a small container. Leave real headroom — if the heap plus overhead exceeds the container limit, the kernel kills the process, and an out-of-memory kill looks like an unexplained restart rather than an error.

Exit code 137 in the event history is the confirmation.

Pick a GC suited to a small heap

G1 is the default and is tuned for larger heaps with multiple cores. On a fraction of a vCPU with a few hundred megabytes, its bookkeeping is overhead you're paying for nothing:

-XX:+UseSerialGC

Serial GC has a smaller footprint and less background work, which is what you want for a bot that spends most of its life idle waiting on a gateway socket. Measure rather than assume, but it's the right first thing to try at this size.

Be realistic about sizing

Honest comparison: a JVM bot needs more than the equivalent Python or Go bot. The runtime itself has a floor before your code does anything, and that floor is higher than an interpreter's and much higher than a static Go binary's.

Practically, that means the Hobby presets rather than the smallest ones — check what you have available on the Limits page. Start at 0.5 GB, watch for restarts, and step up if you see 137s. If you're choosing a language for a new bot specifically to run cheaply, Java isn't the one — but if you already have a JDA bot, it runs fine with the flags above.

FAQ

Can I deploy a Java bot by uploading a zip?

No. The build agent detects Python, Node.js, and prebuilt .NET. Use a Dockerfile with git push or Docker push.

Does the JVM know it's in a container?

Yes. Container support is enabled by default, so the JVM sizes itself from the container's limits rather than the host's.

Why is my Java app using so little memory — or getting killed?

The default heap is a conservative fraction of the container. Set -XX:MaxRAMPercentage explicitly, but leave headroom for non-heap memory or the process gets killed.

Why does my Gradle build fail on the platform but work locally?

The build container has less memory than your app. Use --no-daemon, cap the build JVM heap, or build in CI and push the image.

Which garbage collector should I use?

Try -XX:+UseSerialGC on a small container. G1's overhead is aimed at larger heaps and more cores than you have here.

How much memory does a JDA bot need?

More than a Python or Go equivalent. Start around 0.5 GB and adjust based on whether you see out-of-memory kills.

Wrapping up

The JVM's reputation for being greedy in containers is mostly a defaults problem: it's cautious about the heap, generous about everything around it, and silent about both. Tell it what fraction it may use, leave room for the parts that aren't heap, pick a collector suited to the size, and get the build off the platform if Gradle won't fit. After that a JDA bot is just another long-running process that happens to start a little slower.