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/java pinned to a JDK version.
  • The project's mvnw/gradlew wrapper committed.
  • A named volume for ~/.m2 (Maven) or the Gradle cache.
  • The Java extension pack in the container.

JVM prerequisitesYou need a pinned JDK, a build tool, a dependency-cache volume, and the Java extensions.Java Featurepinned JDKBuild toolMaven or GradleDep cache~/.m2 or gradle volumeExtensionsJava pack

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.

JVM environment layersA pinned JDK, a build tool with a cached dependency store, and the Java language server.JDKFeature-pinned versionBuild toolMaven (.m2) or Gradle cacheDependency cacheon a named volumeLanguage serverjdt.ls routes to the JDK

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.

Setup flowPin the JDK, warm dependencies, cache the store on a volume, then verify a build.Pin JDKJava FeatureWarm depsmvn/gradle downloadCache volume~/.m2 or gradleVerifybuild + test

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

Build time by cacheCaching the dependency store and reusing the build daemon cut JVM build time sharply.Cold Gradle build110sWarm dep cache30sWarm cache + daemon12sillustrative build time

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.

JVM validationConfirm the Java language server uses the container JDK and builds reuse the cache.Does the Java LS use the container JDK?NOSet java.jdt.ls.java.homeDo builds reuse the dep cache?YESJDK version pinnedEditor and build agree

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

JVM pitfall triageA triage path from re-downloading dependencies or JDK mismatch to a deterministic JVM env.Do dependencies re-download each build?YESCache ~/.m2 or gradle on a volumeDoes the LS use a different JDK?YESSet the LS java.homeDeterministic JVM env

SymptomRoot CauseRemediation
Dependencies re-download each buildStore not on a volumeCache ~/.m2 or ~/.gradle on a volume
Editor shows errors the build doesn'tLanguage server on a different JDKSet java.jdt.ls.java.home to the container JDK
Build-tool version differs per machineNot using the wrapperCommit and use mvnw/gradlew
Slow task startupDaemon cold each runReuse the Gradle daemon during interactive work
Cache volume permission errorsVolume owned by rootchown 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.

Pin and cache JVMPin the JDK and wrapper; cache the dependency store and daemon for fast rebuilds.PinJDK versionBuild-tool wrapperLS java.homeCache~/.m2 / gradleBuild daemonWrapper dists

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.