Skip to content
Guides · Starters

How Do I Set Up a Java Dev Environment in a Cloud Sandbox?

Updated October 27, 2026 .md

Short answer: Java isn't preinstalled. Add Adoptium's apt repository and install temurin-25-jdk, plus the JDK your Gradle toolchain asks for. On prod, Spring PetClinic built and tested in 142 seconds with empty caches, about 73 with a warm Gradle cache and daemon, and a build in a sandbox started from a named snapshot was up to date in 17 seconds.

We ran every command on this page on prod on 2026-10-27, on small sandboxes (2 vCPU, 4 GB) in eu-central, plus one default (4 vCPU, 8 GB) for comparison. The project is spring-projects/spring-petclinic at commit 500158f: Spring Boot 4.1, a Gradle 9.5.1 wrapper and a Maven 3.9.16 wrapper side by side, and 81 tests, two of which start MySQL and Postgres in Docker. This is the Java twin of the Python, Node.js, Go and Rust starters. Editor setup over SSH and the SDK from inside the sandbox are covered there.

Results#

Step small default
Install temurin-25-jdk (repo, apt-get update, package) 16.6 s
Install temurin-17-jdk 15.4 s
./gradlew build, empty ~/.gradle (downloads Gradle too) 142.1 s
./gradlew clean build, warm cache, warm daemon (2 runs) 74.9, 70.7 s
./gradlew build, warm cache, new daemon, no build/ (2 runs) 89.5, 87.4 s
./mvnw -B package, empty ~/.m2 71.1 s
./mvnw -B -o package, warm ~/.m2 (2 runs) 49.8, 50.0 s
sbx new --from the snapshot, to ready 3.2 s 3.2 s
From the snapshot: ./gradlew build, new daemon 16.7 s (up to date) 13.9 s
From the snapshot: ./gradlew clean build 83.0 s 68.9 s
From the snapshot: ./mvnw -B -o package 56.1 s 44.4 s

Gradle's build here also runs checkstyle, a format check and a CycloneDX SBOM, so don't read the Gradle and Maven rows as a race between the tools. Two lessons from the numbers:

  • The cache saves about half; the tests are the rest. A warm ~/.gradle cut the build from 142 to about 73 seconds. Most of what remained was the test task. MySqlIntegrationTests alone took 19.0 seconds, because Testcontainers starts a MySQL container.
  • A snapshot keeps build/ too. Gradle found 20 of 21 tasks up to date in the new sandbox and finished in 16.7 seconds, most of it starting a new daemon. The snapshot also held both JDKs, the wrapper's Gradle download and the Docker images.

1. Check for a JDK#

sbx new --name javadev-1 --type small
sbx exec sb_xr89cp0n99 bash -lc "which java javac mvn gradle; java -version; ls /usr/lib/jvm"
ls: cannot access '/usr/lib/jvm': No such file or directory
bash: line 1: java: command not found

No JDK, Maven or Gradle in the image on 2026-10-27. The rest of the inventory is in What's pre-installed in a sandbox. Ubuntu's own openjdk-21-jdk-headless works too, as that page shows. This page uses Eclipse Temurin because Adoptium's apt repository has every LTS line, including 25.

2. Install a current LTS JDK#

Java 25 is the current LTS release. These are Adoptium's documented apt steps. sudo needs no password in a sandbox:

wget -qO - https://packages.adoptium.net/artifactory/api/gpg/key/public | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/adoptium.gpg > /dev/null
echo "deb https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | sudo tee /etc/apt/sources.list.d/adoptium.list
sudo apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq temurin-25-jdk
java -version
deb https://packages.adoptium.net/artifactory/deb noble main
openjdk version "25.0.5" 2026-10-20 LTS
OpenJDK Runtime Environment Temurin-25.0.5+8 (build 25.0.5+8-LTS)
OpenJDK 64-Bit Server VM Temurin-25.0.5+8 (build 25.0.5+8-LTS, mixed mode, sharing)

The whole block took 16.6 seconds. The JDK lands in /usr/lib/jvm/temurin-25-jdk-amd64. That path is outside /home/user, but a stop keeps the whole disk, and a named snapshot saves it too.

SDKMAN is the other common route if you want to switch JDKs per shell without sudo. We didn't test it here.

3. Give Gradle the JDK its toolchain names#

Clone the project at the pinned commit and try a build:

git clone -q https://github.com/spring-projects/spring-petclinic ~/pc && cd ~/pc && git checkout -q 500158f73241
./gradlew --console=plain compileJava
Starting a Gradle Daemon (subsequent builds will be faster)

FAILURE: Build failed with an exception.

* What went wrong:
A problem occurred configuring root project 'spring-petclinic'.
> Failed to calculate the value of task ':compileTestJava' property 'javaCompiler'.
   > Cannot find a Java installation on your machine (Linux 6.18.56-sandboxer amd64) matching: {languageVersion=17, vendor=any vendor, implementation=vendor-specific, nativeImageCapable=false}. Toolchain download repositories have not been configured.
…
BUILD FAILED in 28s

PetClinic's build.gradle sets languageVersion = JavaLanguageVersion.of(17). A Gradle toolchain wants that exact major version, and this project configures no download repository. JDK 25 can run Gradle, but it can't stand in for 17. Install 17 next to it:

sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq temurin-17-jdk
./gradlew -q javaToolchains
 + Eclipse Temurin JDK 17 (17.0.21+9)
     | Language Version:   17
     | Vendor:             Eclipse Temurin
     | Detected by:        Common Linux Locations
 + Eclipse Temurin JDK 25 (25.0.5+8-LTS)
     | Language Version:   25
     | Vendor:             Eclipse Temurin
     | Detected by:        Current JVM

Gradle finds apt-installed JDKs in /usr/lib/jvm on its own. java on the PATH stayed 25. Maven doesn't use toolchains here: the pom sets java.version 17, so ./mvnw compiled for 17 with JDK 25.

4. Build and test: empty, warm and from a snapshot#

We timed each case with a small script that also sampled memory every half second. The cases were:

cd ~/pc
rm -rf ~/.gradle build .gradle && ./gradlew build --console=plain      # empty cache
./gradlew clean build --console=plain                                   # warm cache, warm daemon
./gradlew --stop; rm -rf build && ./gradlew build --console=plain       # warm cache, new daemon
rm -rf ~/.m2 target && ./mvnw -B package                                # Maven, empty cache
rm -rf target && ./mvnw -B -o package                                   # Maven, warm cache, offline
cold1 rc=0 wall=142.1s peakUsedMB=3321
   BUILD SUCCESSFUL in 2m 21s
   21 actionable tasks: 21 executed
warmdaemon1 rc=0 wall=74.9s peakUsedMB=3344
warmdaemon2 rc=0 wall=70.7s peakUsedMB=3323
newdaemon1 rc=0 wall=89.5s peakUsedMB=3112
newdaemon2 rc=0 wall=87.4s peakUsedMB=3101
mvncold1 rc=0 wall=71.1s peakUsedMB=2319
   [INFO] Tests run: 81, Failures: 0, Errors: 0, Skipped: 0
mvnwarm1 rc=0 wall=49.8s peakUsedMB=2645
mvnwarm2 rc=0 wall=50.0s peakUsedMB=2628

Two earlier empty-cache Gradle runs took 157.4 and 137.5 seconds. We don't count them in the table because two Postgres tests failed at the end: we had pointed DOCKER_HOST at a dead socket to skip the container tests, and Spring Boot's Docker Compose support then couldn't start Postgres. With the sandbox's own Docker, all 81 tests passed. Docker is preinstalled, so Testcontainers needs no setup. Pulling postgres:18.4 took 7.1 seconds.

On disk, ~/.gradle was 544 MB and ~/.m2 226 MB. The two container images took 1.8 GB in /var/lib/docker. Then we saved the sandbox:

sbx snapshot sb_xr89cp0n99 javadev-gradle
sbx new --name javadev-2 --type small --from javadev-gradle

The save took 2.1 seconds. Each new sandbox from it was ready in 3.2 seconds, with both JDKs, the caches, build/ and the images in place. The first ./gradlew build there ran only checkFormat and checkstyleNohttp; everything else was up to date. To name snapshots after a hash of your build files and rebuild them when those change, follow Cache dependencies in a named snapshot and hash build.gradle, gradle/wrapper/gradle-wrapper.properties or pom.xml.

5. JVM memory on small and default#

With no -Xmx, the JVM sizes its heap from the machine. Here is what JDK 25 picked in each size:

java -XX:+PrintFlagsFinal -version 2>/dev/null | grep -E " (MaxHeapSize|MaxRAMPercentage|UseG1GC) "
small (4 GB) default (8 GB)
MaxHeapSize 1,027,604,480 bytes (980 MiB) 2,082,471,936 bytes (1,986 MiB)
MaxRAMPercentage 25 25
Garbage collector G1 G1

So a plain java -jar gets a quarter of the sandbox's RAM. The Gradle daemon ignores that default. It started with the same flags on both sizes, and so did the test worker:

java pid=6382 rssMB=654 -XX:MaxMetaspaceSize=384m -Xms256m -Xmx512m org.gradle.launcher.daemon.bootstrap.GradleDaemon
java pid=6759 rssMB=502 -Xmx512m worker.org.gradle.process.internal.worker.GradleWorkerMain
Measured during the build small default
Gradle daemon heap limit -Xmx512m -Xmx512m
Gradle daemon resident memory 483 to 714 MB 511 to 656 MB
Test worker resident memory 445 to 567 MB 494 MB
Peak memory in use, whole sandbox, clean build 3.1 to 3.3 GB of 3.9 GB 3.5 GB of 7.9 GB
clean build from the snapshot 83.0 s 68.9 s

The peak includes the MySQL and Postgres containers and Docker itself. small had about 600 MB to spare, so this build fits, but a bigger test suite or a second container can push it into an out-of-memory kill (exit 137; see Fix exit code 137). If your daemon needs more heap, set it in the project's gradle.properties, as Gradle's docs describe, for example org.gradle.jvmargs=-Xmx1g. On default the extra two cores saved about 14 seconds on the clean build.

6. Run the app on a private URL#

Start the jar bound to 127.0.0.1 in the background, then host the port with --localhost:

sbx exec sb_xr89cp0n99 --cwd pc --detach "java -jar build/libs/spring-petclinic-4.0.0-SNAPSHOT.jar --server.address=127.0.0.1 --server.port=8080 > ~/app.log 2>&1"
sbx host sb_xr89cp0n99 8080 --localhost --title petclinic
{"ok":true,"type":"port.hosted","sandboxId":"sb_xr89cp0n99","port":8080,"url":"https://busy-cobalt-harbor-8080.box.sandboxer.com?_token=<token>","access":"private","isProtected":true,"localhost":true}

The log said Started PetClinicApplication in 6.027 seconds, on Java 25.0.5. From our laptop, without the token, the URL answered 401. The token link answered 302 and set a cookie, and then pages loaded with 200:

no token: 401
token: 302 -> https://busy-cobalt-harbor-8080.box.sandboxer.com/
/ 200 0.498456s
/owners?lastName= 200 0.496762s
/vets.html 200 0.253394s

Those times include the network between our laptop and eu-central. The app's own resident memory grew from 275 MB at startup to 413 MB after 300 requests.

7. Stop and resume with the JVM still warm#

A fresh JVM is slow on its first requests while the JIT compiles. A memory resume skips that. We sent 300 requests from inside the sandbox, then stopped and resumed it:

for i in $(seq 300); do curl -s -o /dev/null -w "%{time_total}\n" "http://127.0.0.1:8080/owners?lastName="; done | sort -n | …
sbx stop sb_xr89cp0n99 --wait
sbx resume sb_xr89cp0n99
sbx info sb_xr89cp0n99 --json
300 warm-up requests: median 0.013335 p90 0.022787
{"ok":true,"type":"sandbox.info","sandbox":{…,"memoryRestored":true,…,"resumeReason":null,…,"state":"ready",…}}
after resume 200 0.008002
after resume 200 0.009464
after resume 200 0.008795

sbx stop --wait took 2.2 seconds and sbx resume 1.35 seconds. The JVM kept its PID (23576) and the kernel kept its boot id, so nothing restarted. The hosted URL answered 200 with the same cookie. For contrast, we then restarted the app:

Started PetClinicApplication in 6.723 seconds (process running for 7.31)
fresh JVM 200 0.739119
fresh JVM 200 0.021229
fresh JVM 200 0.019918

So a resume came back serving in about 9 ms, against 6.7 seconds of startup plus a 739 ms first request for a fresh JVM. A memory resume isn't guaranteed: if memoryRestored is false, the sandbox booted from disk and the --detach process is gone. Run a service that comes back after resume shows the systemd unit for that case.

To stop a detached app, kill the processId that --detach printed. That's the shell wrapping java. Killing that id stops the wrapper and everything it started. When we passed the JVM's own PID instead, on October 27, 2026, the CLI printed "signaled":true and the JVM kept running; sandboxes created after 14:28 UTC that day answer 404 for a PID that --detach didn't start.

FAQ#

Does a Sandboxer sandbox come with Java?#

No. On 2026-10-27 the image had no JDK, Maven or Gradle. Temurin 25 from Adoptium's apt repository installed in 16.6 seconds on small, and Ubuntu's openjdk-21-jdk-headless also works.

Why does Gradle say "Cannot find a Java installation matching languageVersion=17"?#

Your build sets a Java toolchain, and no JDK of that major version is installed. Install it next to your main JDK (temurin-17-jdk here); Gradle detects JDKs in /usr/lib/jvm without configuration.

Is a small sandbox enough for a Spring Boot build?#

For PetClinic, yes. Its full build with MySQL and Postgres Testcontainers peaked at 3.3 GB of 3.9 GB. Gradle's daemon and test worker each run with -Xmx512m on any size; default was about 17% faster on a clean build.

Do ~/.gradle and ~/.m2 survive sbx stop?#

Yes. Both are under /home/user on the sandbox disk, and the JDKs in /usr/lib/jvm are on the same disk. Stop keeps it, and a named snapshot or fork copies it.

Does the JVM stay warm after sbx stop and sbx resume?#

When memoryRestored is true, yes. In our test the same JVM process answered in 8 to 9 ms right after resume, with no restart and no JIT warm-up.

More guides

Top AI sandbox platforms for code execution in 2026, ranked

For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.

Read the guide
Best sandboxes for running coding agents (Claude Code, Codex, Gemini CLI) in 2026, ranked

For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.

Read the guide
Best Daytona sandbox alternatives in 2026, ranked

The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.

Read the guide
Best E2B alternatives in 2026, ranked

If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.

Read the guide