Java & JVM DevContainer Configuration
JVM projects pull large dependency trees and lean on a build daemon, so a reproducible Java devcontainer hinges on pinning the JDK, caching the Maven or Gradle store, and pointing the Java language server at the container's JDK. This guide sets that up for Maven and Gradle alike, so builds are fast and the editor analyzes against the same JDK the build uses. It sits under the language configurations overview.
The reproducibility contract for the JVM is the pinned JDK plus the build tool's wrapper (mvnw/gradlew), which pins the build tool version too. Cache the dependency store on a volume, route the language server, and a Java environment becomes as deterministic as any other in this section.
Prerequisites
You need a pinned JDK via the Java Feature, a build tool (Maven or Gradle, ideally via its wrapper), a named volume for the dependency cache, and the Java extension pack.
ghcr.io/devcontainers/features/javapinned to a JDK version.- The project's
mvnw/gradlewwrapper committed. - A named volume for
~/.m2(Maven) or the Gradle cache. - The Java extension pack in the container.
Architecture & Configuration Deep Dive
Four layers. The JDK is Feature-pinned so every rebuild compiles against the same Java version. The build tool — Maven or Gradle, run through its wrapper — resolves dependencies into a store. That dependency store (~/.m2/repository or the Gradle cache) is cached on a named volume so it isn't refetched. And the Java language server (jdt.ls) must use the container's JDK so its analysis matches the build.
The language-server JDK is the routing detail teams miss: if jdt.ls picks a different JDK than the build, code that compiles can show phantom errors, or vice versa. Set java.jdt.ls.java.home (and the runtime configuration) to the container's JDK so both agree — the same interpreter-routing principle applied to Python and Go elsewhere in this section.
Step-by-Step Implementation
Pin the JDK, warm the dependencies, cache the store on a volume, and verify a build and test run.
{
"name": "Java + Gradle",
"image": "mcr.microsoft.com/devcontainers/base:ubuntu@sha256:PINNED",
"features": { "ghcr.io/devcontainers/features/java:1": { "version": "21", "installGradle": true } },
"customizations": { "vscode": { "extensions": ["vscjava.vscode-java-pack"] } },
"mounts": [
"source=devcontainer-gradle-cache,target=/home/vscode/.gradle,type=volume"
],
"postCreateCommand": "./gradlew --no-daemon dependencies",
"remoteUser": "vscode"
}
Dependency caching for both tools is in caching Maven and Gradle dependencies in a devcontainer, JDK selection in configuring the JDK version in a devcontainer, and debugging in debugging Java in a devcontainer.
Performance & Resource Optimization
JVM build time is dominated by dependency resolution and compilation. Cache the dependency store (~/.m2 or ~/.gradle) on a named volume so a rebuild reuses downloaded artifacts, and reuse the Gradle daemon within a session to avoid JVM startup on every task.
Mount the dependency cache on a volume, and let Gradle's daemon stay warm during interactive work (use --no-daemon only in CI for clean, reproducible runs). The wrapper (gradlew/mvnw) pins the build-tool version so caching never changes behaviour — the same pin-and-cache pattern used across this section's language guides.
Validation & Testing
Confirm the Java language server uses the container JDK (its errors match the build) and that builds reuse the cache rather than re-downloading. A build plus test run against the pinned JDK is the definitive check.
# The JDK the build uses; the LS should match this java.home
java -version
./gradlew --no-daemon build test # reuses the cached dependencies
Common Pitfalls
The failures below are an uncached dependency store or a JDK mismatch. The triage below sorts them.
| Symptom | Root Cause | Remediation |
|---|---|---|
| Dependencies re-download each build | Store not on a volume | Cache ~/.m2 or ~/.gradle on a volume |
| Editor shows errors the build doesn't | Language server on a different JDK | Set java.jdt.ls.java.home to the container JDK |
| Build-tool version differs per machine | Not using the wrapper | Commit and use mvnw/gradlew |
| Slow task startup | Daemon cold each run | Reuse the Gradle daemon during interactive work |
| Cache volume permission errors | Volume owned by root | chown the cache dir to the remote user |
Conclusion
A reproducible JVM devcontainer is a pinned JDK, a wrapper-pinned build tool, a cached dependency store, and a language server pointed at the container's JDK. Cache ~/.m2 or the Gradle cache on a named volume, run through the wrapper so the build-tool version is fixed, and route jdt.ls at the same JDK the build uses — and Java's heavy dependency trees stop slowing every rebuild.
FAQ
How do I speed up Gradle/Maven builds in a devcontainer?
Cache the dependency store on a named volume — ~/.gradle for Gradle or ~/.m2/repository for Maven — so rebuilds reuse downloaded artifacts instead of refetching them. Keep the Gradle daemon warm during interactive work to skip JVM startup, reserving --no-daemon for clean CI runs.
Why does the Java editor show errors my build doesn't?
The Java language server (jdt.ls) is analyzing against a different JDK than the build uses. Set java.jdt.ls.java.home and the runtime configuration to the container's JDK so the language server and the build compile against the same Java version, and the phantom errors disappear.
Should I pin the build tool as well as the JDK?
Yes — use the project's mvnw/gradlew wrapper, which pins the exact Maven or Gradle version. Combined with a Feature-pinned JDK, that makes both the compiler and the build tool reproducible, so a cached dependency store can never change how the project builds.
Related
- Language-Specific Environment Configurations — the overview of reproducible language runtimes.
- Caching Maven and Gradle Dependencies in a DevContainer — the dependency-store caching detail.
- Configuring the JDK Version in a DevContainer — pinning and routing the JDK.
- Debugging Java in a DevContainer — attaching the debugger to the container JVM.
- Python DevContainer Setup with Poetry & venv — the same pin-and-route pattern for Python.