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

> 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.

Source: https://sandboxer.com/guides/java-dev-environment-in-a-cloud-sandbox. Updated 2026-10-27.

**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](https://github.com/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](/guides/python-dev-environment-in-a-cloud-sandbox), [Node.js](/guides/node-dev-environment-in-a-cloud-sandbox), [Go](/guides/go-dev-environment-in-a-cloud-sandbox) and [Rust](/guides/rust-dev-environment-in-a-cloud-sandbox) 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

```bash
sbx new --name javadev-1 --type small
sbx exec sb_xr89cp0n99 bash -lc "which java javac mvn gradle; java -version; ls /usr/lib/jvm"
```

```text
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](/guides/whats-preinstalled-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](https://adoptium.net/installation/linux/). `sudo` needs no password in a sandbox:

```bash
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
```

```text
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](https://sdkman.io/) 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:

```bash
git clone -q https://github.com/spring-projects/spring-petclinic ~/pc && cd ~/pc && git checkout -q 500158f73241
./gradlew --console=plain compileJava
```

```text
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](https://docs.gradle.org/current/userguide/toolchains.html) 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:

```bash
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq temurin-17-jdk
./gradlew -q javaToolchains
```

```text
 + 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:

```bash
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
```

```text
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:

```bash
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](/guides/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:

```bash
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:

```text
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](/guides/fix-exit-code-137-or-a-full-disk-in-a-sandbox)). If your daemon needs more heap, set it in the project's `gradle.properties`, as [Gradle's docs](https://docs.gradle.org/current/userguide/build_environment.html) 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`:

```bash
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
```

```text
{"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`:

```text
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:

```bash
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
```

```text
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:

```text
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](/guides/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.

## Related

- [Set up a Python dev environment with uv](/guides/python-dev-environment-in-a-cloud-sandbox)
- [Set up a Node.js dev environment with pnpm](/guides/node-dev-environment-in-a-cloud-sandbox)
- [Cache dependencies in a named snapshot](/guides/cache-dependencies-in-a-named-snapshot)
- [Pause and resume a sandbox with memory](/guides/pause-and-resume-a-sandbox-with-memory)
