Maven Properties, Profiles, settings.xml, Mirrors, Proxies, Servers, and Environment-Specific Builds: Guided Hands-On Workflow and Core Operations
Build a disposable Maven project, inspect effective profiles/settings/properties, compare project, settings-profile, and command-line property values, and route one fixed dependency through a local authenticated mirror without embedding credentials.
Learning objectives
- Create a disposable Maven project and a local authenticated Maven-style repository using only the JDK plus the project wrapper.
-
Prove project/profile/settings/CLI property values with
help:evaluaterather than guessing precedence. - Inspect active profiles, effective settings, effective POM state, and dependency tree before and after changes.
- Route one fixed dependency through a narrow localhost mirror while keeping credentials outside project files.
- Compare warm resolution with a fresh isolated local repository and explain exactly which cache/repository state changed.
~/.m2, global
settings, shared CI settings, or real credentials are modified.
./mvnw, grep, sha256sum, and
shell environment variables. On Windows use mvnw.cmd,
Select-String, Get-FileHash, and
$env:NAME. Maven profile/settings semantics are
cross-platform; file paths and shell quoting are not.
1. Lab contract and directory layout
The mandatory path is local and disposable. Maven still needs normal
access to Central for its own plugins unless your normal
organization policy already mirrors Central; the illustrative
dependency itself comes from a JDK-served localhost repository. The
mirror matches only repository ID academy-remote, so it
cannot accidentally capture Central.
mkdir -p maven-environment-lab/{src/main/java/dev/academy,fixture-src/dev/academy/fixture,fixture-classes,lab-repository,lab,evidence}
cd maven-environment-lab
export REPO="$PWD/.lab-m2/repository"
mkdir -p "$REPO"
If you do not already have the trusted wrapper from Chapter 04, bootstrap it once with an installed Maven and then use the wrapper for all subsequent commands:
mvn org.apache.maven.plugins:maven-wrapper-plugin:3.3.4:wrapper -Dmaven=3.9.16 -Dtype=only-script
./mvnw -v
2. Author the portable project first
The POM intentionally declares a repository ID with an unusable
.invalid URL. The lab will prove that settings can
redirect this ID to the localhost mirror without changing the
dependency coordinate or committing environment credentials.
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dev.academy</groupId>
<artifactId>environment-boundary-lab</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.build.outputTimestamp>2026-08-23T00:00:00Z</project.build.outputTimestamp>
<academy.channel>project-default</academy.channel>
<academy.environment>portable</academy.environment>
</properties>
<repositories>
<repository>
<id>academy-remote</id>
<url>https://academy.invalid/maven2</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>false</enabled></snapshots>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>dev.academy.fixture</groupId>
<artifactId>academy-fixture</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-clean-plugin</artifactId>
<version>3.5.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.5.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.15.0</version>
<configuration><release>${maven.compiler.release}</release></configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.6</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.5.1</version>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>dev</id>
<properties>
<academy.environment>dev</academy.environment>
<academy.channel>pom-dev</academy.channel>
</properties>
</profile>
<profile>
<id>ci</id>
<properties>
<academy.environment>ci</academy.environment>
<academy.channel>pom-ci</academy.channel>
</properties>
</profile>
</profiles>
</project>
package dev.academy;
import dev.academy.fixture.Fixture;
public final class App {
private App() {}
public static void main(String[] args) {
System.out.println("environment-boundary-lab -> " + Fixture.message());
}
}
The dev and ci profiles only change
properties that are not consumed by compilation or packaging. This
is deliberate: the build can expose environment selection without
creating environment-specific artifact bytes.
3. Seed a tiny Maven-style repository with JDK tools
A remote Maven release artifact is just repository content arranged by coordinate path. Build a tiny fixed JAR and its POM, then place both under the canonical repository layout. This is not a publishing lesson; it is a transparent resolution fixture.
package dev.academy.fixture;
public final class Fixture {
private Fixture() {}
public static String message() {
return "fixture-1.0.0";
}
}
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dev.academy.fixture</groupId>
<artifactId>academy-fixture</artifactId>
<version>1.0.0</version>
</project>
javac --release 17 -d fixture-classes fixture-src/dev/academy/fixture/Fixture.java
mkdir -p lab-repository/dev/academy/fixture/academy-fixture/1.0.0
jar --create --file lab-repository/dev/academy/fixture/academy-fixture/1.0.0/academy-fixture-1.0.0.jar -C fixture-classes .
cp fixture-pom.xml lab-repository/dev/academy/fixture/academy-fixture/1.0.0/academy-fixture-1.0.0.pom
for f in lab-repository/dev/academy/fixture/academy-fixture/1.0.0/*.{jar,pom}; do
sha1sum "$f" | awk '{print $1}' > "$f.sha1"
done
find lab-repository -type f -maxdepth 8 -print
The immutable dependency identity is now
dev.academy.fixture:academy-fixture:1.0.0. Its bytes
are visible before Maven downloads anything.
4. Serve the fixture with local Basic authentication
The JDK includes the jdk.httpserver module. The fixture
server binds only to loopback, prevents path traversal, requires
Basic authentication, and never logs the Authorization header. It is
not a production repository manager; it exists only to make
server-ID behavior observable.
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;
public final class AuthRepoServer {
private AuthRepoServer() {}
public static void main(String[] args) throws Exception {
if (args.length != 1) throw new IllegalArgumentException("repository root required");
String user = System.getenv("ACADEMY_REPO_USER");
String pass = System.getenv("ACADEMY_REPO_PASSWORD");
if (user == null || pass == null) throw new IllegalStateException("lab credentials not set");
Path root = Paths.get(args[0]).toAbsolutePath().normalize();
String expected = "Basic " + Base64.getEncoder().encodeToString((user + ":" + pass).getBytes(StandardCharsets.UTF_8));
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 18080), 0);
server.createContext("/", exchange -> serve(exchange, root, expected));
server.start();
System.out.println("Academy fixture repository: http://127.0.0.1:18080/");
}
private static void serve(HttpExchange exchange, Path root, String expected) throws IOException {
String auth = exchange.getRequestHeaders().getFirst("Authorization");
if (!expected.equals(auth)) {
exchange.getResponseHeaders().set("WWW-Authenticate", "Basic realm=academy-lab");
exchange.sendResponseHeaders(401, -1);
exchange.close();
return;
}
String requestPath = exchange.getRequestURI().getPath();
Path file = root.resolve(requestPath.substring(1)).normalize();
if (!file.startsWith(root) || !Files.isRegularFile(file)) {
exchange.sendResponseHeaders(404, -1);
exchange.close();
return;
}
Headers headers = exchange.getResponseHeaders();
headers.set("Content-Type", "application/octet-stream");
long size = Files.size(file);
exchange.sendResponseHeaders(200, size);
if (!"HEAD".equalsIgnoreCase(exchange.getRequestMethod())) {
Files.copy(file, exchange.getResponseBody());
}
exchange.close();
}
}
javac --add-modules jdk.httpserver AuthRepoServer.java
export ACADEMY_REPO_USER=academy-lab
read -r -s -p "Disposable lab password: " ACADEMY_REPO_PASSWORD; echo
export ACADEMY_REPO_PASSWORD
java --add-modules jdk.httpserver AuthRepoServer lab-repository
5. Create alternate settings with mirror and credential indirection
The repository contains no credential value. Settings references
process environment variables. The mirrorOf pattern is
an exact ID, so only academy-remote is redirected.
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0 https://maven.apache.org/xsd/settings-1.2.0.xsd">
<mirrors>
<mirror>
<id>academy-mirror</id>
<name>Disposable localhost Maven mirror</name>
<url>http://127.0.0.1:18080/</url>
<mirrorOf>academy-remote</mirrorOf>
</mirror>
</mirrors>
<servers>
<server>
<id>academy-mirror</id>
<username>${env.ACADEMY_REPO_USER}</username>
<password>${env.ACADEMY_REPO_PASSWORD}</password>
</server>
</servers>
<profiles>
<profile>
<id>lab-settings</id>
<properties>
<academy.channel>settings-profile</academy.channel>
</properties>
</profile>
</profiles>
<activeProfiles>
<activeProfile>lab-settings</activeProfile>
</activeProfiles>
</settings>
Notice the identity chain: POM repository ID
academy-remote → mirror match → mirror ID
academy-mirror → server credentials keyed to
academy-mirror.
6. Inspect settings and profiles before building
Use -s lab/settings-good.xml on every Maven invocation
so there is no ambiguity about which user settings are in scope.
mkdir -p evidence
./mvnw -s lab/settings-good.xml org.apache.maven.plugins:maven-help-plugin:3.5.2:active-profiles -Doutput=evidence/active-profiles-settings.txt
./mvnw -s lab/settings-good.xml org.apache.maven.plugins:maven-help-plugin:3.5.2:effective-settings -Doutput=evidence/effective-settings.xml
./mvnw -s lab/settings-good.xml org.apache.maven.plugins:maven-help-plugin:3.5.2:effective-pom -Dverbose -Doutput=evidence/effective-pom-settings.xml
grep -nE 'academy-mirror|academy-remote|lab-settings|academy.channel' evidence/effective-settings.xml evidence/effective-pom-settings.xml
7. Observe property layering with one expression
The POM declares project-default. The active settings
profile contributes settings-profile. The explicit POM
profile dev contributes pom-dev. Current
Maven profile guidance documents that settings profile effects can
take priority over POM profile values in the effective build; a CLI
user property can override the final value. Verify rather than
memorize.
HELP=org.apache.maven.plugins:maven-help-plugin:3.5.2
./mvnw -s lab/settings-good.xml "$HELP:evaluate" -Dexpression=academy.channel -q -DforceStdout
./mvnw -s lab/settings-good.xml -Pdev "$HELP:evaluate" -Dexpression=academy.channel -q -DforceStdout
./mvnw -s lab/settings-good.xml -Pdev "$HELP:evaluate" -Dexpression=academy.channel -Dacademy.channel=cli -q -DforceStdout
| Invocation | Expected value | Why |
|---|---|---|
| Settings active, no POM profile | settings-profile |
Settings profile contributes the property. |
Settings active + -Pdev |
settings-profile |
The active settings-profile contribution wins over the conflicting POM-profile property. |
Same + -Dacademy.channel=cli |
cli |
CLI user property is the explicit invocation override. |
8. Resolve/build through the mirror
Now Maven must obtain academy-fixture:1.0.0. The
declared .invalid URL should never be contacted because
the exact repository ID is mirrored to localhost.
set -o pipefail
./mvnw -s lab/settings-good.xml -Dmaven.repo.local="$REPO" -Pdev clean package | tee evidence/build-cold.log
./mvnw -s lab/settings-good.xml -Dmaven.repo.local="$REPO" org.apache.maven.plugins:maven-dependency-plugin:3.11.0:tree -DoutputFile=evidence/dependency-tree.txt
cat evidence/dependency-tree.txt
find "$REPO/dev/academy/fixture/academy-fixture/1.0.0" -maxdepth 1 -type f -print
java -cp "target/environment-boundary-lab-1.0.0.jar:$REPO/dev/academy/fixture/academy-fixture/1.0.0/academy-fixture-1.0.0.jar" dev.academy.App
Expected application output is
environment-boundary-lab -> fixture-1.0.0. The local
repository now contains a cached copy plus resolver metadata. The
authoritative fixture remains the localhost repository; the cache is
only derived machine/job state.
9. Warm-cache versus fresh-cache causality
Run the same package command again. Dependency transfer should be reduced because the isolated local repository is warm. Then prove the remote path again with a second empty repository instead of deleting the first.
set -o pipefail
./mvnw -s lab/settings-good.xml -Dmaven.repo.local="$REPO" -Pdev package | tee evidence/build-warm.log
export REPO_FRESH="$PWD/.lab-m2-fresh/repository"
./mvnw -s lab/settings-good.xml -Dmaven.repo.local="$REPO_FRESH" -Pdev package | tee evidence/build-fresh.log
This distinction matters in CI: a warm cache can hide mirror/proxy/server failures. A second isolated repository gives you a clean-room resolution test without destroying evidence from the first run.
10. Challenge — choose the correct control
A teammate wants the dev build to use a different
repository URL and proposes putting that URL inside the
dev POM profile. Choose a better control and justify
it.
dev only for a build-model difference that truly
belongs to the project.
11. Cleanup
Stop the localhost server with Ctrl+C. Remove only the disposable
maven-environment-lab tree when finished. Do not delete
your normal Maven local repository or rewrite normal user/global
settings. Preserve evidence/ elsewhere first if you
want to compare later chapters.
Knowledge check
Why does the POM use an unusable
.invalid repository URL in this lab?
To prove that the selected settings mirror—not the POM URL—is
the actual transport endpoint for repository ID
academy-remote.
What changed after the first successful cold build?
The isolated local repository gained artifact/plugin metadata and cached bytes; the project source/POM and remote fixture identity did not change.
Why is a second isolated repository better than deleting the first cache for a fresh-resolution test?
It preserves evidence and avoids destructive troubleshooting while still proving the remote path from an empty state.
Where is the real lab password stored in the project files?
Nowhere. The settings file contains only an environment-variable reference, and the actual disposable value exists in process environment memory.
Which command proves that
-Dacademy.channel=cli won?
Evaluate that exact expression with the Help Plugin under the same settings/profile invocation and record the resulting value.
12. Bridge to design choices
You now have a concrete boundary: project model, settings model, repository routing, credentials, and cache state are separately visible. Lesson 3 asks where each kind of variation should live in a real team and what tradeoffs follow from that placement.
Official references and version notes
Version-sensitive statements in this lesson were checked against Apache Maven primary documentation on 2026-08-23. The mandatory path uses Maven 3.9.16 through Maven Wrapper 3.3.4, JDK 21 to run Maven, Java 17 as the project release target, Help Plugin 3.5.2, Dependency Plugin 3.11.0, Compiler Plugin 3.15.0, Resources Plugin 3.5.0, Surefire 3.5.6, and JAR Plugin 3.5.1. Maven 4-only profile syntax and preview behavior are not required here.
The localhost Basic-auth server is a teaching fixture built from JDK APIs; it is not an Apache Maven component and must not be treated as a production repository manager.
- Maven — Settings Reference
- Maven — Introduction to Build Profiles
- Maven — POM Reference / Properties
- Maven — Using Mirrors for Repositories
- Maven — Setting up Multiple Repositories
- Maven — Security and Deployment Settings
- Maven — Injecting POM Properties via settings.xml
- Maven Help Plugin 3.5.2
- Maven Dependency Plugin 3.11.0
- Maven Compiler Plugin 3.15.0
- Maven JAR Plugin 3.5.1
- Maven Resources Plugin 3.5.0
- Maven Surefire 3.5.6
- Apache Maven Wrapper
- Maven 3.9.16 Release Notes
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.