ZetCode

Maven deploying artifacts

last modified November 13, 2026

In this article we deploy artifacts with Maven. We deploy releases and snapshots to a repository, look at the files that Maven writes there, attach sources and javadoc JARs, sign the artifacts with GPG, and build a bundle for Maven Central. The examples use Maven 3.10, Deploy plugin 3.2.0, GPG plugin 3.2.8, and Java 25.

The install phase copies the artifacts of a project to the local repository in ~/.m2/repository, where other projects on the same machine find them. The deploy phase, which comes right after it, uploads them to a remote repository, where other developers and CI servers find them. The upload is done by the Maven Deploy plugin.

The project

We deploy a small library.

mathlib/src/main/java/com/example/mathlib/MathUtils.java
package com.example.mathlib;

/**
 * Small math helpers.
 */
public final class MathUtils {

    private MathUtils() {
    }

    /**
     * Returns the greatest common divisor of two numbers.
     *
     * @param a the first number
     * @param b the second number
     * @return the greatest common divisor
     */
    public static long gcd(long a, long b) {
        return b == 0 ? Math.abs(a) : gcd(b, a % b);
    }
}

The class has one method with Javadoc comments, which we need later for the javadoc JAR.

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>local-releases</id>
            <url>file://${project.basedir}/../repo/releases</url>
        </repository>
        <snapshotRepository>
            <id>local-snapshots</id>
            <url>file://${project.basedir}/../repo/snapshots</url>
        </snapshotRepository>
    </distributionManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-deploy-plugin</artifactId>
                <version>3.2.0</version>
            </plugin>
        </plugins>
    </build>
</project>

The distributionManagement section tells Maven where to deploy. The repository element is used for releases, the snapshotRepository element for versions that end with -SNAPSHOT.

<url>file://${project.basedir}/../repo/releases</url>

A repository is any location that Maven can write to. In practice it is an HTTP repository manager such as Nexus, Artifactory, or Reposilite; the Maven repositories and settings.xml article deploys to a local HTTP server and shows how credentials are stored in settings.xml under the repository id. To see the result directly in the file system, we use file:// URLs pointing to a repo directory next to the project; they need no server and no credentials.

Deploying a release

We run the build up to the deploy phase.

Maven uploads the POM, the JAR, and the maven-metadata.xml file, together with MD5 and SHA-1 checksums of each file. The metadata file lists all versions of the artifact; it is how Maven resolves version ranges and finds the latest release. The last message comes from the final pass of the plugin, which deploys the projects configured with deployAtEnd; our project was deployed already.

Release versions should never change once published. A file repository accepts a second deployment of 1.0.0 and overwrites the files, but repository managers are usually configured to reject it, and Maven Central never allows it.

$ mvn deploy
--- deploy:3.2.0:deploy (default-deploy) @ mathlib ---
Uploading to local-releases: file://.../repo/releases/com/example/mathlib/1.0.0/mathlib-1.0.0.pom
Uploading to local-releases: file://.../repo/releases/com/example/mathlib/1.0.0/mathlib-1.0.0.jar
Uploading to local-releases: file://.../repo/releases/com/example/mathlib/maven-metadata.xml
Skipping already-deployed com.example:mathlib:1.0.0
BUILD SUCCESS
$ find ../repo -type f | sort
../repo/releases/com/example/mathlib/1.0.0/mathlib-1.0.0.jar
../repo/releases/com/example/mathlib/1.0.0/mathlib-1.0.0.jar.md5
../repo/releases/com/example/mathlib/1.0.0/mathlib-1.0.0.jar.sha1
../repo/releases/com/example/mathlib/1.0.0/mathlib-1.0.0.pom
../repo/releases/com/example/mathlib/1.0.0/mathlib-1.0.0.pom.md5
../repo/releases/com/example/mathlib/1.0.0/mathlib-1.0.0.pom.sha1
../repo/releases/com/example/mathlib/maven-metadata.xml
../repo/releases/com/example/mathlib/maven-metadata.xml.md5
../repo/releases/com/example/mathlib/maven-metadata.xml.sha1
$ cat ../repo/releases/com/example/mathlib/maven-metadata.xml
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
  <groupId>com.example</groupId>
  <artifactId>mathlib</artifactId>
  <versioning>
    <release>1.0.0</release>
    <versions>
      <version>1.0.0</version>
    </versions>
    <lastUpdated>20261113133819</lastUpdated>
  </versioning>
</metadata>

Deploying snapshots

A snapshot is a development version that can be deployed many times. We change the version to 1.1.0-SNAPSHOT and deploy twice.

The snapshot goes to the snapshot repository. The SNAPSHOT part of the file name is replaced with a UTC timestamp and a build number, so every deployment creates new files. The metadata of the version records the latest timestamp and build number. A project that depends on 1.1.0-SNAPSHOT reads this file and downloads the latest build; by default Maven checks for a newer snapshot once a day, which can be forced with the -U option. Old snapshot builds pile up; repository managers delete them with cleanup policies.

$ mvn deploy
Uploading to local-snapshots: file://.../repo/snapshots/com/example/mathlib/1.1.0-SNAPSHOT/mathlib-1.1.0-20261113.133824-2.pom
Uploading to local-snapshots: file://.../repo/snapshots/com/example/mathlib/1.1.0-SNAPSHOT/mathlib-1.1.0-20261113.133824-2.jar
Uploading to local-snapshots: file://.../repo/snapshots/com/example/mathlib/1.1.0-SNAPSHOT/maven-metadata.xml
Uploading to local-snapshots: file://.../repo/snapshots/com/example/mathlib/maven-metadata.xml
BUILD SUCCESS
$ find ../repo/snapshots -name '*.jar' | sort
../repo/snapshots/com/example/mathlib/1.1.0-SNAPSHOT/mathlib-1.1.0-20261113.133821-1.jar
../repo/snapshots/com/example/mathlib/1.1.0-SNAPSHOT/mathlib-1.1.0-20261113.133824-2.jar
$ cat ../repo/snapshots/com/example/mathlib/1.1.0-SNAPSHOT/maven-metadata.xml
<?xml version="1.0" encoding="UTF-8"?>
<metadata modelVersion="1.1.0">
  <groupId>com.example</groupId>
  <artifactId>mathlib</artifactId>
  <versioning>
    <lastUpdated>20261113133824</lastUpdated>
    <snapshot>
      <timestamp>20261113.133824</timestamp>
      <buildNumber>2</buildNumber>
    </snapshot>
    <snapshotVersions>
      <snapshotVersion>
        <extension>pom</extension>
        <value>1.1.0-20261113.133824-2</value>
        <updated>20261113133824</updated>
      </snapshotVersion>
      <snapshotVersion>
        <extension>jar</extension>
        <value>1.1.0-20261113.133824-2</value>
        <updated>20261113133824</updated>
      </snapshotVersion>
    </snapshotVersions>
  </versioning>
  <version>1.1.0-SNAPSHOT</version>
</metadata>

Deploying elsewhere

The altDeploymentRepository property overrides the repository from the POM. Its value has the id::url format; the id selects the credentials in settings.xml.

The artifacts went to target/staging. This is useful in CI pipelines that deploy to a repository that the POM does not know about, and for checking what a deployment would upload. The maven.deploy.skip property skips the deployment, for example for modules of a multi-module project that are not meant to be published, such as tests or examples.

$ mvn deploy -DaltDeploymentRepository=staging::file:target/staging
Uploading to staging: file:target/staging/com/example/mathlib/1.0.0/mathlib-1.0.0.pom
Uploading to staging: file:target/staging/com/example/mathlib/1.0.0/mathlib-1.0.0.jar
Uploading to staging: file:target/staging/com/example/mathlib/maven-metadata.xml
BUILD SUCCESS
$ mvn deploy -Dmaven.deploy.skip=true
--- deploy:3.2.0:deploy (default-deploy) @ mathlib ---
Skipping artifact deployment
BUILD SUCCESS

Sources and javadoc JARs

Libraries are usually published with their source code and API documentation, which IDEs download to show sources and documentation of the library classes. The Source and Javadoc plugins create these JARs.

pom.xml (build plugins)
<plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-deploy-plugin</artifactId>
        <version>3.2.0</version>
    </plugin>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-source-plugin</artifactId>
        <version>3.4.0</version>
        <executions>
            <execution>
                <id>attach-sources</id>
                <goals>
                    <goal>jar-no-fork</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-javadoc-plugin</artifactId>
        <version>3.12.0</version>
        <executions>
            <execution>
                <id>attach-javadocs</id>
                <goals>
                    <goal>jar</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
</plugins>

The jar-no-fork goal of the Source plugin packs src/main/java into a JAR with the sources classifier. The jar goal of the same plugin would first fork the lifecycle up to the generate-sources phase, which runs those plugins twice. The jar goal of the Javadoc plugin generates the HTML documentation and packs it with the javadoc classifier. Both goals run in the package phase by default.

Both JARs are attached to the project, so the install and deploy phases handle them together with the main JAR. The version was changed to 1.2.0.

$ mvn deploy
--- jar:3.5.1:jar (default-jar) @ mathlib ---
--- source:3.4.0:jar-no-fork (attach-sources) @ mathlib ---
--- javadoc:3.12.0:jar (attach-javadocs) @ mathlib ---
--- deploy:3.2.0:deploy (default-deploy) @ mathlib ---
Uploading to local-releases: file://.../repo/releases/com/example/mathlib/1.2.0/mathlib-1.2.0.pom
Uploading to local-releases: file://.../repo/releases/com/example/mathlib/1.2.0/mathlib-1.2.0.jar
Uploading to local-releases: file://.../repo/releases/com/example/mathlib/1.2.0/mathlib-1.2.0-sources.jar
Uploading to local-releases: file://.../repo/releases/com/example/mathlib/1.2.0/mathlib-1.2.0-javadoc.jar
Uploading to local-releases: file://.../repo/releases/com/example/mathlib/maven-metadata.xml
BUILD SUCCESS
$ unzip -l target/mathlib-1.2.0-sources.jar | grep java
      424  1980-02-01 00:00   com/example/mathlib/MathUtils.java
$ unzip -l target/mathlib-1.2.0-javadoc.jar | grep -c html
12

Signing with GPG

Maven Central and many other repositories require a PGP signature for every file, so that users can check that the files come from the publisher. The Maven GPG plugin signs the artifacts with the gpg tool. For the example we created a key without a passphrase with gpg --quick-gen-key 'Example Developer <dev@example.com>' ed25519 sign never.

pom.xml (GPG plugin)
<artifactId>maven-gpg-plugin</artifactId>
<version>3.2.8</version>
<executions>
    <execution>
        <id>sign-artifacts</id>
        <phase>verify</phase>
        <goals>
            <goal>sign</goal>
        </goals>
    </execution>
</executions>

The sign goal runs in the verify phase, after all JARs are packaged. It signs the main artifact, the POM, and all attached artifacts.

The plugin uses the default key of gpg; the gpg.keyname property selects another one. Since version 3.2, the passphrase of a protected key is read from the MAVEN_GPG_PASSPHRASE environment variable. Each file gets a detached .asc signature, which is deployed with it, and gpg --verify checks a file against its signature. A real release key is published on a key server, such as keys.openpgp.org, so that anyone can verify the signatures.

$ gpg --list-secret-keys
sec   ed25519 2026-11-13 [SC]
      B07A59AFBED205DB18859ED0B8E5BFA6FB07AEEC
uid           [ultimate] Example Developer <dev@example.com>
$ mvn deploy
--- gpg:3.2.8:sign (sign-artifacts) @ mathlib ---
Signer 'gpg' is signing 4 files with key default
Uploading to local-releases: file://.../repo/releases/com/example/mathlib/1.3.0/mathlib-1.3.0.pom
Uploading to local-releases: file://.../repo/releases/com/example/mathlib/1.3.0/mathlib-1.3.0.jar
Uploading to local-releases: file://.../repo/releases/com/example/mathlib/1.3.0/mathlib-1.3.0-sources.jar
Uploading to local-releases: file://.../repo/releases/com/example/mathlib/1.3.0/mathlib-1.3.0-javadoc.jar
Uploading to local-releases: file://.../repo/releases/com/example/mathlib/1.3.0/mathlib-1.3.0.jar.asc
Uploading to local-releases: file://.../repo/releases/com/example/mathlib/1.3.0/mathlib-1.3.0.pom.asc
Uploading to local-releases: file://.../repo/releases/com/example/mathlib/1.3.0/mathlib-1.3.0-sources.jar.asc
Uploading to local-releases: file://.../repo/releases/com/example/mathlib/1.3.0/mathlib-1.3.0-javadoc.jar.asc
Uploading to local-releases: file://.../repo/releases/com/example/mathlib/maven-metadata.xml
BUILD SUCCESS
$ gpg --verify target/mathlib-1.3.0.jar.asc target/mathlib-1.3.0.jar
gpg: Signature made Fri 13 Nov 2026 02:38:18 PM CET
gpg:                using EDDSA key B07A59AFBED205DB18859ED0B8E5BFA6FB07AEEC
gpg: Good signature from "Example Developer <dev@example.com>" [ultimate]

Publishing to Maven Central

Maven Central is run by Sonatype and accepts uploads through the Central Portal at central.sonatype.com. Publishing requires an account, a verified namespace for the group ID, and a token stored in settings.xml under the server id central. Central also checks the POM: it must have a name, description, URL, license, developer, and SCM information, and every file must come with sources, javadoc, signatures, and checksums.

pom.xml (project information)
<name>mathlib</name>
<description>Small math helpers</description>
<url>https://github.com/example/mathlib</url>

<licenses>
    <license>
        <name>Apache License, Version 2.0</name>
        <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
    </license>
</licenses>

<developers>
    <developer>
        <name>Example Developer</name>
        <email>dev@example.com</email>
    </developer>
</developers>

<scm>
    <connection>scm:git:https://github.com/example/mathlib.git</connection>
    <url>https://github.com/example/mathlib</url>
</scm>

These elements describe the project on the Central search pages.

pom.xml (Central plugin)
<artifactId>central-publishing-maven-plugin</artifactId>
<version>0.12.0</version>
<extensions>true</extensions>
<configuration>
    <publishingServerId>central</publishingServerId>
    <skipUpload>true</skipUpload>
</configuration>

The Central Publishing plugin replaces the Deploy plugin. With extensions set to true, it hooks into the deploy phase. The distributionManagement section is not needed. The skipUpload option creates the bundle without uploading it; we use it because we have no account for the com.example namespace. In a real project, the option is removed, and the bundle is uploaded and validated. The deployment then waits in the portal for manual publishing, unless autoPublish is set to true.

The plugin collects the artifacts, signatures, and the POM, adds MD5, SHA-1, SHA-256, and SHA-512 checksums, and zips them in the repository layout. Once a release is published on Central, it can never be changed or deleted.

$ mvn deploy
--- gpg:3.2.8:sign (sign-artifacts) @ mathlib ---
--- central-publishing:0.12.0:publish (injected-central-publishing) @ mathlib ---
Staging 7 files
Generate checksums for dir: com/example/mathlib/1.4.0
Skipping the upload of the bundle at user's request (skipUpload). The bundle is at
  /home/user/project/mathlib/target/central-publishing/central-bundle.zip
BUILD SUCCESS
$ unzip -Z1 target/central-publishing/central-bundle.zip | sort
com/example/mathlib/1.4.0/mathlib-1.4.0.jar
com/example/mathlib/1.4.0/mathlib-1.4.0.jar.asc
com/example/mathlib/1.4.0/mathlib-1.4.0.jar.md5
com/example/mathlib/1.4.0/mathlib-1.4.0.jar.sha1
com/example/mathlib/1.4.0/mathlib-1.4.0.jar.sha256
com/example/mathlib/1.4.0/mathlib-1.4.0.jar.sha512
com/example/mathlib/1.4.0/mathlib-1.4.0-javadoc.jar
com/example/mathlib/1.4.0/mathlib-1.4.0-javadoc.jar.asc
com/example/mathlib/1.4.0/mathlib-1.4.0-javadoc.jar.md5
com/example/mathlib/1.4.0/mathlib-1.4.0-javadoc.jar.sha1
com/example/mathlib/1.4.0/mathlib-1.4.0-javadoc.jar.sha256
com/example/mathlib/1.4.0/mathlib-1.4.0-javadoc.jar.sha512
com/example/mathlib/1.4.0/mathlib-1.4.0.pom
com/example/mathlib/1.4.0/mathlib-1.4.0.pom.asc
com/example/mathlib/1.4.0/mathlib-1.4.0.pom.md5
com/example/mathlib/1.4.0/mathlib-1.4.0.pom.sha1
com/example/mathlib/1.4.0/mathlib-1.4.0.pom.sha256
com/example/mathlib/1.4.0/mathlib-1.4.0.pom.sha512
com/example/mathlib/1.4.0/mathlib-1.4.0-sources.jar
com/example/mathlib/1.4.0/mathlib-1.4.0-sources.jar.asc
com/example/mathlib/1.4.0/mathlib-1.4.0-sources.jar.md5
com/example/mathlib/1.4.0/mathlib-1.4.0-sources.jar.sha1
com/example/mathlib/1.4.0/mathlib-1.4.0-sources.jar.sha256
com/example/mathlib/1.4.0/mathlib-1.4.0-sources.jar.sha512

Useful options

PropertyDescription
altDeploymentRepositorydeploy to id::url instead of the POM repository
maven.deploy.skipskip the deployment
deployAtEnddeploy all modules after the last one is built
retryFailedDeploymentCountretry failed uploads
gpg.skipskip signing
gpg.keynamekey used for signing

With deployAtEnd, a multi-module build deploys nothing when any module fails, instead of leaving a half-deployed release in the repository.

Source

Apache Maven Deploy Plugin

In this article we have deployed releases and snapshots with Maven, attached sources and javadoc JARs, signed the artifacts with GPG, and built a bundle for Maven Central.

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.