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.
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.
<?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.
<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.
<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.
<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.
<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
| Property | Description |
|---|---|
altDeploymentRepository | deploy to id::url instead of the POM repository |
maven.deploy.skip | skip the deployment |
deployAtEnd | deploy all modules after the last one is built |
retryFailedDeploymentCount | retry failed uploads |
gpg.skip | skip signing |
gpg.keyname | key 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
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
List all Java tutorials.