Maven repositories and settings.xml
last modified November 9, 2026
In this article we work with Maven repositories and the
settings.xml file. We look into the local repository, deploy a
library to a private repository, consume it from another project, route all
downloads through a mirror, and keep the credentials out of the POM. The
examples use Maven 3.10 and Java 25.
Maven distinguishes two kinds of repositories. The local
repository is a directory on our machine, ~/.m2/repository
by default, which caches downloaded artifacts and holds the artifacts we
install. Remote repositories are accessed over HTTP; the default
one is Maven Central. Companies usually run their own remote repositories
with a repository manager such as Nexus, Artifactory, or Reposilite.
The settings files
The POM describes a project and is shared by everyone who builds it. The
settings.xml file configures Maven on a particular machine or
for a particular user: where the local repository is, which mirrors to use,
and the credentials for the servers. Maven reads two settings files and
merges them; the user settings win.
${maven.home}/conf/settings.xml, the global settings of the Maven installation~/.m2/settings.xml, the user settings
The -s option replaces the user settings file and
-gs the global one. In this article, we keep the settings files
in the directory of the examples and pass them with -s, so that
our real configuration stays untouched. The help:effective-settings
goal shows the merged result.
On a machine without a user settings file, the effective settings contain the default local repository, the plugin groups searched for plugin prefixes, and a blocked mirror from the global settings, which we explain later.
$ mvn help:effective-settings
<settings xmlns="http://maven.apache.org/SETTINGS/1.3.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.3.0 https://maven.apache.org/xsd/settings-1.3.0.xsd">
<localRepository>/home/user/.m2/repository</localRepository>
<mirrors>
<mirror>
<mirrorOf>external:http:*</mirrorOf>
<name>Pseudo repository to mirror external repositories initially using HTTP.</name>
<url>http://0.0.0.0/</url>
<blocked>true</blocked>
<id>maven-default-http-blocker</id>
</mirror>
</mirrors>
<pluginGroups>
<pluginGroup>org.apache.maven.plugins</pluginGroup>
<pluginGroup>org.codehaus.mojo</pluginGroup>
</pluginGroups>
</settings>
The local repository
The local repository uses the same layout as remote repositories. An artifact is stored in a directory made of its group ID, with dots replaced by slashes, its artifact ID, and its version.
The directory of Commons Text 1.15.0 contains the JAR, the POM, and their
checksums, which Maven verifies on download. The
_remote.repositories file records from which repository the
files came; Maven uses it to make sure that an artifact downloaded from one
repository is not used by a project that does not have access to that
repository. Its location can be changed with the
localRepository element of the settings or with
-Dmaven.repo.local.
$ ls ~/.m2/repository/org/apache/commons/commons-text/1.15.0/ commons-text-1.15.0.jar commons-text-1.15.0.jar.md5 commons-text-1.15.0.jar.sha1 commons-text-1.15.0.pom commons-text-1.15.0.pom.md5 commons-text-1.15.0.pom.sha1 _remote.repositories $ cat ~/.m2/repository/org/apache/commons/commons-text/1.15.0/_remote.repositories #NOTE: This is a Maven Resolver internal implementation file, its format can be changed without prior notice. #Sun Nov 08 21:16:09 CET 2026 commons-text-1.15.0.jar>central-8fac3ebd6edbaca3c794783fa38088af0aa128e7= commons-text-1.15.0.pom>central-8fac3ebd6edbaca3c794783fa38088af0aa128e7=
A private repository
To have a private repository to deploy to, we use a small Python script, which serves files over HTTP. A real repository manager does the same, with a user interface, access control, and search on top.
#!/usr/bin/env python3
"""A tiny Maven repository server for experiments.
/releases/... hosted repository; requires the user deployer:s3cret
/central/... proxy of Maven Central with a local cache; no authentication
"""
import base64, os, sys, urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
ROOT = os.path.abspath(sys.argv[2] if len(sys.argv) > 2 else 'storage')
CENTRAL = 'https://repo.maven.apache.org/maven2'
TOKEN = 'Basic ' + base64.b64encode(b'deployer:s3cret').decode()
class Handler(BaseHTTPRequestHandler):
def log_message(self, fmt, *args):
print(f'{self.command} {self.path} {args[1]}', flush=True)
def file_path(self):
return os.path.join(ROOT, self.path.lstrip('/'))
def authorized(self):
if not self.path.startswith('/releases/'):
return True
if self.headers.get('Authorization') == TOKEN:
return True
self.send_response(401)
self.send_header('WWW-Authenticate', 'Basic realm="releases"')
self.send_header('Content-Length', '0')
self.end_headers()
return False
def fetch_central(self, path):
url = CENTRAL + self.path[len('/central'):]
try:
with urllib.request.urlopen(url) as resp:
data = resp.read()
except Exception:
return False
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'wb') as f:
f.write(data)
return True
def do_GET(self, body=True):
if not self.authorized():
return
path = self.file_path()
if not os.path.isfile(path) and self.path.startswith('/central/'):
self.fetch_central(path)
if not os.path.isfile(path):
self.send_response(404)
self.send_header('Content-Length', '0')
self.end_headers()
return
with open(path, 'rb') as f:
data = f.read()
self.send_response(200)
self.send_header('Content-Length', str(len(data)))
self.end_headers()
if body:
self.wfile.write(data)
def do_HEAD(self):
self.do_GET(body=False)
def do_PUT(self):
if not self.authorized():
return
path = self.file_path()
os.makedirs(os.path.dirname(path), exist_ok=True)
length = int(self.headers['Content-Length'])
with open(path, 'wb') as f:
f.write(self.rfile.read(length))
self.send_response(201)
self.send_header('Content-Length', '0')
self.end_headers()
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8081
ThreadingHTTPServer(('localhost', port), Handler).serve_forever()
Paths under /releases form a hosted repository. Reading and
writing require the user deployer with the password
s3cret, sent with HTTP Basic authentication. Paths under
/central form a proxy of Maven Central: a missing file is
downloaded from Central, stored, and served. Every request is logged.
We start the server in a separate terminal with
python3 repo_server.py 8081 storage; it listens on port 8081
and keeps the files in the storage directory.
Deploying a library
The deploy phase uploads the artifacts of a project to the
repository defined in the distributionManagement section.
<?xml version="1.0" encoding="UTF-8"?>
<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>com.example</groupId>
<artifactId>mathlib</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>25</maven.compiler.release>
</properties>
<distributionManagement>
<repository>
<id>company-releases</id>
<url>http://localhost:8081/releases</url>
</repository>
</distributionManagement>
</project>
The repository has the ID company-releases. The ID connects
the repository with its credentials in the settings. A
snapshotRepository element, not used here, would receive the
snapshot versions.
package com.example.mathlib;
import java.util.Arrays;
public class Stats {
public static double mean(double... values) {
return Arrays.stream(values).average().orElse(Double.NaN);
}
}
The library has a single method, which computes the arithmetic mean.
<?xml version="1.0" encoding="UTF-8"?>
<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">
<servers>
<server>
<id>company-releases</id>
<username>deployer</username>
<password>s3cret</password>
</server>
</servers>
</settings>
The server element gives the credentials for the repository
with the same ID. Credentials belong to the settings, not to the POM, which
is committed to version control and published with the artifacts.
The -B option runs Maven in batch mode, without the progress of
the transfers. Without settings, the server rejects the upload with status
code 401. With the settings, Maven uploads the POM and the JAR, and it
creates the maven-metadata.xml file, which lists the versions
of the artifact.
$ mvn -B deploy [INFO] Uploading to company-releases: http://localhost:8081/releases/com/example/mathlib/1.0.0/mathlib-1.0.0.pom [INFO] Uploading to company-releases: http://localhost:8081/releases/com/example/mathlib/1.0.0/mathlib-1.0.0.jar [ERROR] BUILD FAILURE [ERROR] ... Could not transfer artifact com.example:mathlib:pom:1.0.0 from/to company-releases (http://localhost:8081/releases): HTTP Status: 401 $ mvn -B deploy -s ../settings.xml [INFO] Uploaded to company-releases: http://localhost:8081/releases/com/example/mathlib/1.0.0/mathlib-1.0.0.pom [INFO] Uploaded to company-releases: http://localhost:8081/releases/com/example/mathlib/1.0.0/mathlib-1.0.0.jar [INFO] Uploaded to company-releases: http://localhost:8081/releases/com/example/mathlib/maven-metadata.xml [INFO] BUILD SUCCESS $ find ../server/storage -name '*.jar' -o -name '*.pom' ../server/storage/releases/com/example/mathlib/1.0.0/mathlib-1.0.0.pom ../server/storage/releases/com/example/mathlib/1.0.0/mathlib-1.0.0.jar
Using the private repository
A project that depends on the library must know the repository. Additional
repositories are declared in the repositories section of the
POM; Maven Central is always searched as well.
<?xml version="1.0" encoding="UTF-8"?>
<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>com.example</groupId>
<artifactId>calc</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>25</maven.compiler.release>
</properties>
<repositories>
<repository>
<id>company-releases</id>
<url>http://localhost:8081/releases</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>mathlib</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
</project>
The repository uses the same ID, so the same server credentials apply. Snapshots are disabled for it; Maven then does not ask the repository for snapshot versions.
package com.example.calc;
import com.example.mathlib.Stats;
public class Main {
public static void main(String[] args) {
System.out.println(Stats.mean(3, 4, 5, 9));
}
}
The program prints the mean of four numbers.
We removed the library from the local repository after deploying it, so
Maven downloads it from the private repository. The
_remote.repositories file now names
company-releases as the source.
$ mvn -B compile exec:java -Dexec.mainClass=com.example.calc.Main -s ../settings.xml [INFO] Downloaded from company-releases: http://localhost:8081/releases/com/example/mathlib/1.0.0/mathlib-1.0.0.pom [INFO] Downloaded from company-releases: http://localhost:8081/releases/com/example/mathlib/1.0.0/mathlib-1.0.0.jar 5.25 [INFO] BUILD SUCCESS $ cat ~/.m2/repository/com/example/mathlib/1.0.0/_remote.repositories #NOTE: This is a Maven Resolver internal implementation file, its format can be changed without prior notice. #Mon Nov 09 22:16:29 CET 2026 mathlib-1.0.0.jar>company-releases-d4765deeb4954f740c9d8da184c5dcf2da29420b= mathlib-1.0.0.pom>company-releases-d4765deeb4954f740c9d8da184c5dcf2da29420b=
Mirrors
A mirror replaces a repository: requests for the mirrored repository go to the mirror URL instead. Companies use a mirror of Maven Central to cache downloads, to keep builds working when Central is unreachable, and to control which artifacts are allowed.
<?xml version="1.0" encoding="UTF-8"?>
<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">
<localRepository>${user.dir}/local-repo</localRepository>
<mirrors>
<mirror>
<id>company-proxy</id>
<mirrorOf>central</mirrorOf>
<url>http://localhost:8081/central</url>
</mirror>
</mirrors>
</settings>
The mirrorOf element selects the mirrored repositories by ID.
The value central redirects Maven Central to our proxy. Other
values are * for all repositories,
external:* for all repositories except those on localhost and
file repositories, and lists such as *,!company-releases. The
localRepository element points to an empty directory in the
current project, so that every artifact has to be downloaded.
<?xml version="1.0" encoding="UTF-8"?>
<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>com.example</groupId>
<artifactId>words</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.15.0</version>
</dependency>
</dependencies>
</project>
The project has a single dependency on Commons Text.
All downloads went through the company-proxy mirror, including
the plugins needed to run dependency:tree. The server log
counts the requests to the proxy. If the mirror required credentials, they
would be given in a server element with the ID of the
mirror.
$ mvn -B dependency:tree -s ../settings-mirror.xml [INFO] Downloaded from company-proxy: http://localhost:8081/central/org/apache/commons/commons-text/1.15.0/commons-text-1.15.0.pom [INFO] com.example:words:jar:1.0.0 [INFO] \- org.apache.commons:commons-text:jar:1.15.0:compile [INFO] \- org.apache.commons:commons-lang3:jar:3.20.0:compile [INFO] BUILD SUCCESS $ grep -c 'GET /central' ../server/server.log 500 $ ls local-repo pom.xml
Blocked HTTP repositories
Since Maven 3.8.1, the global settings contain the
maven-default-http-blocker mirror shown in the effective
settings above. It mirrors external:http:*, which means all
repositories that use plain HTTP and are not on localhost, and it is marked
as blocked. Downloading over HTTP would allow an attacker on the network to
replace artifacts. Our server on localhost is not affected.
We change the URL of the repository in the consumer project to an HTTP address on another host. The build fails without any network access: the request goes to the blocked mirror. The fix is to use HTTPS; if that is not possible, the user settings can define a mirror with the same ID that overrides the blocker.
$ mvn -B compile [ERROR] Could not transfer artifact com.example:mathlib:pom:1.0.0 from/to maven-default-http-blocker (http://0.0.0.0/): Blocked mirror for repositories: [company-releases (http://repo.example.com/maven2, default, releases)]
Encrypted passwords
Plain-text passwords in settings.xml can be encrypted. First we
encrypt a master password and store it in the
settings-security.xml file, by default
~/.m2/settings-security.xml. Then we encrypt the server
passwords with the master password and put them into the settings.
<?xml version="1.0" encoding="UTF-8"?>
<settingsSecurity>
<master>{ZxILKZWR7KwH7Hmas8XeBK1nAmKKSUxWSPR17+1nrXk=}</master>
</settingsSecurity>
The file holds the encrypted master password. The
-Dsettings.security property points Maven to another location
of the file; we use it to keep the file in the directory of the examples.
<servers>
<server>
<id>company-releases</id>
<username>deployer</username>
<password>{a8X6aWHyxXUHgi8bpKLOwKXcOqKm6mndkQTkx7Ozdpg=}</password>
</server>
</servers>
The encrypted password is enclosed in curly braces.
The --encrypt-master-password and
--encrypt-password options print the encrypted values; the
output differs on each run, because the encryption uses a random salt. The
deployment works with the encrypted password. This protects the password
from a casual look at the file, but anyone who can read both files can
decrypt it.
$ mvn -q --encrypt-master-password masterpass
{dJ0Hz+kKOIcH4MBnEsmxOtlwt8rUms2GxSVVKAyP85w=}
$ mvn -q --encrypt-password s3cret -Dsettings.security=settings-security.xml
{XDxREsSHfd8Hdy8fNtxl/KTvddJ2rmcfK7sVXwpAZ7c=}
$ cd mathlib
$ mvn -B deploy -s ../settings-enc.xml -Dsettings.security=../settings-security.xml
[INFO] Uploaded to company-releases: http://localhost:8081/releases/com/example/mathlib/1.0.0/mathlib-1.0.0.pom
[INFO] Uploaded to company-releases: http://localhost:8081/releases/com/example/mathlib/1.0.0/mathlib-1.0.0.jar
[INFO] Uploaded to company-releases: http://localhost:8081/releases/com/example/mathlib/maven-metadata.xml
[INFO] BUILD SUCCESS
Credentials from environment variables
On CI servers, credentials are usually provided as secret environment
variables. The settings file can refer to them with the
${env.NAME} syntax.
<servers>
<server>
<id>company-releases</id>
<username>${env.REPO_USER}</username>
<password>${env.REPO_PASSWORD}</password>
</server>
</servers>
The user name and the password are read from the REPO_USER
and REPO_PASSWORD variables when Maven starts. The settings
file contains no secret and can be committed to the repository of the
project, for example as .mvn/settings.xml, and passed with
-s.
With the variables set, the deployment succeeds.
$ cd mathlib $ export REPO_USER=deployer REPO_PASSWORD=s3cret $ mvn -B deploy -s ../settings-env.xml [INFO] Uploaded to company-releases: http://localhost:8081/releases/com/example/mathlib/1.0.0/mathlib-1.0.0.pom [INFO] Uploaded to company-releases: http://localhost:8081/releases/com/example/mathlib/1.0.0/mathlib-1.0.0.jar [INFO] Uploaded to company-releases: http://localhost:8081/releases/com/example/mathlib/maven-metadata.xml [INFO] BUILD SUCCESS
Best practices
- Keep repository URLs in the POM or in a parent POM and credentials in
settings.xml; never put passwords into a POM. - Use the same ID for a repository, its server credentials, and its mirror.
- In companies, route all downloads through a repository manager with a
mirror of
*orexternal:*. - Use HTTPS for all remote repositories.
- On CI servers, read credentials from environment variables.
- Inspect the merged configuration with
help:effective-settingswhen downloads go to an unexpected place.
Source
In this article we have worked with local and remote repositories, deployed to a private repository, configured a mirror, and managed credentials in the settings.
Author
List all Java tutorials.