ZetCode

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.

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.

server/repo_server.py
#!/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.

mathlib/pom.xml
<?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.

mathlib/src/main/java/com/example/mathlib/Stats.java
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.

settings.xml
<?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.

calc/pom.xml
<?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.

calc/src/main/java/com/example/calc/Main.java
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.

settings-mirror.xml
<?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.

words/pom.xml
<?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.

settings-security.xml
<?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.

settings-enc.xml (servers)
<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.

settings-env.xml (servers)
<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

Source

Maven settings reference

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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Java tutorials.