Maven Works Locally, Fails on CI: Reproducibility Debug
Fast recall
Quick revision
Core answer
~/.m2/settings.xml vs CI settings; (2) snapshot vs release version drift — local has cached old SNAPSHOT, CI fetches new broken one; (3) auth credentials missing on CI for private Maven repo (Nexus, Artifactory); (4) JDK or Maven version mismatch. Fix: pin Maven version in CI, mount a settings.xml with auth, prefer release versions over SNAPSHOTs, run mvn -U to force update locally to reproduce.Practical debugging of build environment differences — a common real-world issue
Adding the dependency to a different repo without checking why CI didn't see it; cargo-culting fixes without diagnosing
Discuss reproducible builds, settings.xml profile management, the SNAPSHOT-version trap, dependency mediation, and using `mvn dependency:tree` to debug
Interview-ready guide
Interview answer
Maven failing on CI but passing locally is almost always environment drift, not a Maven bug. The five most common causes, in order of frequency:
The five usual suspects
- Private repo not configured on CI — your local settings.xml has a server entry for Nexus or Artifactory; CI runs without that file
- SNAPSHOT version drift — local has cached an old SNAPSHOT from last week; CI fetches the latest one published yesterday with a breaking change
- Auth credentials missing on CI — the repo is configured but credentials aren't passed via env vars, Jenkins credentials, or GitHub secrets
- JDK or Maven version mismatch — local Maven 3.9 with JDK 17; CI uses Maven 3.6 with JDK 11; some plugins behave differently
- Local cache hides a missing dependency — a transitive is in your local .m2 from another project but isn't actually declared; CI starts fresh and can't find it
The systematic debug
Step 1 — reproduce locally with a clean cache
- Run
mvn -U clean installto force re-fetch ignoring cache, or remove the failing artifact from~/.m2/repository - If it now fails locally, your cache was lying — the issue is reproducible
- If it still passes, the issue is CI-specific configuration
Step 2 — compare effective settings
- Run
mvn help:effective-settingsandmvn help:effective-pomon both local and CI; diff the output - Look for different repositories, different servers, different mirrors, different profiles activated
Step 3 — inspect the dependency tree
mvn dependency:tree -Dverboseshows omitted and conflicting versions; look for dependency mediation hiding the real version
Step 4 — mount settings.xml on CI with auth
- Use templates or actions like
s4u/maven-settings-actionto inject credentials from secrets - Or template settings.xml at build time using environment variables
Step 5 — pin Maven and JDK
- Use Maven Wrapper (
./mvnw); bundles a specific Maven version with the project so local and CI run the same - Pin JDK via
actions/setup-javawith a specific version like17.0.10
Step 6 — address SNAPSHOT drift
- Pin to release versions in production code paths
- Consider
maven-lockfile-pluginto freeze resolved versions to a lockfile, similar to package-lock.json
The pitfalls to call out
- Local cache is a liability for reproducibility — it speeds builds but masks missing dependencies; periodically clear it or test with
-U - Maven dependency resolution is order-dependent — different repo order in settings.xml gives different versions
- SNAPSHOT versions are non-deterministic — a team that publishes 1.0-SNAPSHOT 50 times a day will have someone's build break tomorrow
- Don't store plaintext passwords in settings.xml — use encrypted master passwords or environment-variable templating
The strategic answer
- The root cause of most local-vs-CI drift is that the local environment isn't pinned to match CI
- Maven Wrapper plus pinned JDK plus mounted settings.xml plus release versions over SNAPSHOTs eliminates most of these issues structurally
- For full reproducibility, adopt a dependency lockfile plugin so the resolved versions are frozen explicitly
Maven Works Locally, Fails on CI: Reproducibility Debug
Direct Answer
Almost always one of: (1) the dependency resolves from a private repo not configured on CI — check ~/.m2/settings.xml vs CI settings; (2) snapshot vs release version drift — local has cached old SNAPSHOT, CI fetches new broken one; (3) auth credentials missing on CI for private Maven repo (Nexus, Artifactory); (4) JDK or Maven version mismatch. Fix: pin Maven version in CI, mount a settings.xml with auth, prefer release versions over SNAPSHOTs, run mvn -U to force update locally to reproduce.
Common Mistakes & Pitfalls
Adding the dependency to a different repo without checking why CI didn't see it; cargo-culting fixes without diagnosing
Check your understanding
Answer these without looking back at the article.
- How does Maven Wrapper (`./mvnw`) work and why is it preferred over a system Maven?
- What's the difference between SNAPSHOT and release versions in Maven?
- How does `maven-lockfile-plugin` provide reproducible builds?
- What is dependency mediation and how does Maven choose between conflicting versions?
Related interview prep categories
1Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.