Maven Profiles
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Apache Maven 3.10.0. Every example was run on JDK 26 on 2026-10-16 in a scratch workspace that mimics the
tidepool build, on macOS (so the mac OS profile below was active). Where Maven 3.9.16 or Maven 4.0.0-rc-7 (a release candidate) was
run too, the text says so. Console output was trimmed to the relevant lines, and the lines Loaded N auto-discovered prefixes ...
that Maven 3.10 prints while resolving were left out.
A profile is a named set of POM changes that is applied only when the profile is active. It exists because Maven tries hard to make a
build portable, and some builds cannot be: a different JDK needs another compiler flag, a CI machine needs another repository, a
developer needs another database URL. Profiles are the escape hatch, and, as the last section explains, an escape hatch that should be
used sparingly. This page covers where profiles are declared, what they can change, how they are switched on and off, in which order they
are applied and which traps they set. The settings.xml file that can also hold profiles is on
settings.xml & Password Encryption, and the
POM elements that a profile overrides are on POM Fundamentals.
Where profiles live
A profile has an id and, optionally, an activation section plus the content it applies. There are two places to declare one:
-
In
pom.xml, in aprofileselement. This is the portable place: the profile travels with the project, and it can change almost any part of the POM (see What a profile can change). A profile declared in a parent POM is inherited by the children (see Inheritance, Aggregation & the Effective POM). -
In
settings.xml(~/.m2/settings.xmlfor the user,${maven.home}/conf/settings.xmlfor the installation). These profiles belong to a machine or a person, not to a project, so Maven lets them change only a few things:properties,repositoriesandpluginRepositories.
The profiles.xml file that Maven 2 read next to the POM is no longer supported. A profiles.xml placed beside a POM was ignored by
Maven 3.10.0, with the warning Detected profiles.xml alongside io.tidepool:t:1, this file is no longer supported and was ignored,
please use the settings.xml instead.
A minimal profile for tidepool that switches the station data source and renames the jar:
<profiles>
<profile>
<id>env-live</id>
<properties>
<station.url>https://stations.example.org/api/v2</station.url>
</properties>
<build>
<finalName>tidepool-live</finalName>
</build>
</profile>
</profiles>
Together with <station.url>file:data/stations.csv</station.url> in the POM’s own properties and a filtered app.properties resource
(see Resources & Filtering), the build with and without
the profile gave:
$ mvn -q package
station.url=file:data/stations.csv
target/tidepool-env-1.0-SNAPSHOT.jar
$ mvn -q package -Penv-live
station.url=https://stations.example.org/api/v2
target/tidepool-live.jar
(the lines after the commands are the content of target/classes/app.properties and a listing of the jar.)
What a profile can change
A profile in the POM may contain these elements, which replace or extend their namesakes in the POM:
-
repositories,pluginRepositories,dependencies,dependencyManagement,distributionManagement,modules,properties,reportingandreports -
a subset of
build:defaultGoal,resources,testResources,directory,finalName,filters,pluginsandpluginManagement
Anything else in build, such as sourceDirectory or outputDirectory, is rejected as a malformed POM. This was run with a profile
that contained <build><sourceDirectory>other</sourceDirectory></build>, and Maven 3.9.16, 3.10.0 and 4.0.0-rc-7 all refused to build:
[ERROR] Malformed POM ./pom.xml: Unrecognised tag: 'sourceDirectory' (position: START_TAG seen ...</id>\n <build><sourceDirectory>... @7:31) @ ./pom.xml, line 7, column 31
[ERROR] The build could not read 1 project -> [Help 1]
(Maven 4 words the message differently: Encountered 1 problem(s) while processing the POMs.)
A profile in settings.xml is more restricted still, because it cannot be shared with the project’s other developers. Giving a settings
profile an element it does not know, such as a build element, does not fail the build in Maven 3.10.0, it prints a warning and ignores
the element:
[WARNING] Some problems were encountered while building the effective settings
[WARNING] Unrecognised tag: 'build' (position: START_TAG seen ...</properties>\n <build>... @8:14) @ .../settings-bad.xml, line 8, column 14
A settings profile that is allowed (properties and a repository) and is made active through activeProfiles:
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0">
<profiles>
<profile>
<id>team-defaults</id>
<properties>
<station.url>https://stations.example.org/team</station.url>
</properties>
<repositories>
<repository>
<id>team-mirror</id>
<url>https://repo.example.org/team</url>
</repository>
</repositories>
</profile>
</profiles>
<activeProfiles>
<activeProfile>team-defaults</activeProfile>
</activeProfiles>
</settings>
Run with mvn -s settings-example.xml -q help:evaluate -Dexpression=station.url -DforceStdout the property was
https://stations.example.org/team, the value from the settings profile, even though the POM declared another default. It was also the
value when the POM profile env-live was added with -Penv-live: a settings profile is applied after the profiles of the POM, so it wins
(the order is the subject of Profile order and precedence). Selecting ${project.repositories} showed team-mirror listed before
the central repository inherited from the super POM.
Activating a profile
Maven evaluates profiles when it builds the model of each project, before any lifecycle phase runs. A profile becomes active in exactly one of these ways:
The help:active-profiles goal of the Help plugin lists the profiles that are really active for each project of a build, and where each
one was declared. It is the first thing to run when a profile "does not work".
Explicit activation with -P
-P (long form --activate-profiles) takes a comma-separated list of profile ids. The profiles are activated in addition to the ones
that activate themselves. The tidepool sample POM below has profiles for each activation type; run with no options on JDK 26 and macOS,
mvn help:active-profiles listed:
The following profiles are active:
- jdk21plus (source: io.tidepool:tidepool-profiles:1.0-SNAPSHOT)
- mac (source: io.tidepool:tidepool-profiles:1.0-SNAPSHOT)
- no-offline (source: io.tidepool:tidepool-profiles:1.0-SNAPSHOT)
- has-file (source: io.tidepool:tidepool-profiles:1.0-SNAPSHOT)
- pom-only (source: io.tidepool:tidepool-profiles:1.0-SNAPSHOT)
and mvn help:active-profiles -Plive added the explicitly named profile in front of them:
The following profiles are active:
- live (source: io.tidepool:tidepool-profiles:1.0-SNAPSHOT)
- jdk21plus (source: io.tidepool:tidepool-profiles:1.0-SNAPSHOT)
- mac (source: io.tidepool:tidepool-profiles:1.0-SNAPSHOT)
- no-offline (source: io.tidepool:tidepool-profiles:1.0-SNAPSHOT)
- has-file (source: io.tidepool:tidepool-profiles:1.0-SNAPSHOT)
- pom-only (source: io.tidepool:tidepool-profiles:1.0-SNAPSHOT)
The full POM used for these listings (it is a pom packaging project with a file data/stations.csv):
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-profiles</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<station.source>bundled</station.source>
</properties>
<profiles>
<profile>
<id>default-on</id>
<activation><activeByDefault>true</activeByDefault></activation>
<properties><flavor>default</flavor></properties>
</profile>
<profile>
<id>live</id>
<properties><station.source>live</station.source></properties>
</profile>
<profile>
<id>jdk21plus</id>
<activation><jdk>[21,)</jdk></activation>
<properties><jdk.note>21-or-newer</jdk.note></properties>
</profile>
<profile>
<id>mac</id>
<activation><os><family>mac</family></os></activation>
<properties><os.note>mac</os.note></properties>
</profile>
<profile>
<id>by-property</id>
<activation>
<property><name>tide.site</name><value>pier</value></property>
</activation>
<properties><site.note>pier-selected</site.note></properties>
</profile>
<profile>
<id>no-offline</id>
<activation>
<property><name>!skipData</name></property>
</activation>
<properties><data.note>data-enabled</data.note></properties>
</profile>
<profile>
<id>has-file</id>
<activation>
<file><exists>${basedir}/data/stations.csv</exists></file>
</activation>
<properties><file.note>csv-found</file.note></properties>
</profile>
<profile>
<id>pom-only</id>
<activation>
<property><name>packaging</name><value>pom</value></property>
</activation>
<properties><pack.note>pom-packaging</pack.note></properties>
</profile>
<profile>
<id>both</id>
<activation>
<jdk>[21,)</jdk>
<property><name>tide.site</name></property>
</activation>
<properties><both.note>both-conditions</both.note></properties>
</profile>
</profiles>
</project>
activeByDefault
<activeByDefault>true</activeByDefault> activates a profile when nothing else is active. In the listing above the profile
default-on is missing, although it is declared activeByDefault: the moment any other profile of the same POM is activated (here by
JDK, OS, file and so on), every activeByDefault profile of that POM is switched off. It is the best known surprise of profile
activation, and it makes activeByDefault unreliable in a POM that also has implicit profiles. A minimal POM with a dev profile
active by default and a ci profile activated by the property env.CI behaved like this on Maven 3.10.0 and 4.0.0-rc-7:
$ mvn help:active-profiles
- dev
$ mvn help:active-profiles -Denv.CI=true
- ci
(the list was reduced to the profile ids). The same result came from the environment variable, CI=true mvn help:active-profiles, because
the env. prefix gives a property access to the environment.
settings.xml activeProfiles
A profile id listed in the activeProfiles element of settings.xml is active for every build that can see that settings file. This is
the usual way to switch on a settings profile, since a profile in settings.xml has no way to activate itself on a project property that
only exists in the POM. The example above used it. activeByDefault does not replace it for resolver options: Maven builds the resolver
session before profiles are evaluated, so system properties such as aether.* set in a settings profile only take effect if the profile
is selected with activeProfiles or -P.
JDK activation
The jdk element activates on the version of the JDK that runs Maven:
<activation>
<jdk>[21,)</jdk>
</activation>
A value that starts with [ or ( is a version range, so [21,) is "21 or newer" and [1.3,1.6) is the half-open range from 1.3 to
1.6. A value that does not start with a bracket is a prefix match against the Java version: 26 matches 26 and 26.0.1, and a
leading ! negates the prefix, so !26 is any version that does not start with 26. The Maven documentation uses a vendor prefix such as
zulu64 as an example. A range is compared with the version as it is, so an upper bound such as 1.5] likely excludes the patch
releases of 1.5 (1.5.0_05). Run on JDK 26, a profile with <jdk>26</jdk> was active, <jdk>!26</jdk> and <jdk>[1.3,1.6)</jdk> were not,
and jdk21plus was active.
OS activation
The os element has up to four conditions: name, family, arch and version, which are compared with the Java system
properties os.name, os.arch and os.version; the family values (windows, mac, unix and a few others) are the same as in the
Maven Enforcer plugin, which the Maven guide points to for details. All the given conditions must match, and each can be negated with
!. On the Apple silicon machine used here, os.arch is aarch64, so this profile was active, and <family>!windows</family> was
active too:
<activation>
<os>
<family>mac</family>
<arch>aarch64</arch>
</os>
</activation>
Property activation
The property element activates the profile when a system property, a user property given on the command line with -D, or an
environment property (env.NAME) exists, or has a given value:
<activation>
<property>
<name>tide.site</name>
<value>pier</value>
</property>
</activation>
Without value, the profile is active when the property is set to any value. A name that starts with ! is active when the property
is not set, and a value that starts with ! is active when the property is unset or has another value (a test profile with
<value>!a</value> was active without x, with -Dx=b, and inactive with -Dx=a). The listing above showed the first two forms:
by-property and both appeared only with -Dtide.site=pier, and no-offline (!skipData) disappeared with -DskipData:
$ mvn help:active-profiles -Dtide.site=pier
- jdk21plus
- mac
- by-property
- no-offline
- has-file
- pom-only
- both
$ mvn help:active-profiles -DskipData
- jdk21plus
- mac
- has-file
- pom-only
(again reduced to the profile ids.) The properties of the POM itself are not visible to activation, only system and user properties
and the environment are, plus the special property packaging described next: a profile that tested tide.site equal to pier was not
activated by a <tide.site>pier</tide.site> entry in the POM’s own properties element.
Packaging property
Since Maven 3.9.0, the property name packaging evaluates to the packaging of the project being built. It is meant for a profile in a
common parent POM that must apply only to some of the children:
<activation>
<property>
<name>packaging</name>
<value>war</value>
</property>
</activation>
There is no packaging element in activation. A first attempt with <activation><packaging>pom</packaging></activation> failed on
Maven 3.10.0 with a real parse error:
[ERROR] Malformed POM .../pom.xml: Unrecognised tag: 'packaging' (position: START_TAG seen ...<activation><packaging>... @47:30) @ .../pom.xml, line 47, column 30
The pom-only profile in the listing above uses the property form and was active for the pom packaging project on Maven 3.9.16,
3.10.0 and 4.0.0-rc-7.
File activation
The file element activates on a file that exists or is missing:
<activation>
<file>
<exists>${basedir}/data/stations.csv</exists>
</file>
</activation>
Only a few variables are interpolated here: ${project.basedir} (and the older ${basedir}), system properties such as ${user.home}
and environment variables as ${env.HOME}. Properties and values defined in the POM, such as ${project.build.directory}, are not
available, so a path below target has to be written out. The has-file profile was active in the run above.
Multiple conditions
Several different activation types can be combined in one profile, and the profile is active only if all of them hold. The both
profile above has jdk and property, and it appeared only when tide.site was set. Repeating the same type twice in one profile
is not supported (the Maven issues MNG-5909 and MNG-3328 track it), so "JDK 17 or 21" needs two profiles, or the Maven 4 condition
element described below.
Deactivating a profile and unknown profiles
A profile that would otherwise be active, including an activeByDefault one or one from activeProfiles, can be switched off on the
command line by putting ! or - before its id:
mvn help:active-profiles -P '!jdk21plus'
mvn help:active-profiles -P=-jdk21plus
The ! is special for most shells, so quote it or escape it; there is also a bug in the command-line parser (CLI-309) with values that
start with a minus, which is why the second form, -P=-id, is recommended. Run on Maven 3.10.0, -P '!jdk21plus' removed
jdk21plus from the list and left the others.
What happens when the id does not exist differs by release:
$ mvn -B help:active-profiles -Pnope # Maven 3.9.16 and 3.10.0
[WARNING] The requested profile "nope" could not be activated because it does not exist.
...
$ mvn -B help:active-profiles -Pnope # Maven 4.0.0-rc-7
[ERROR] The requested profiles [nope] could not be activated or deactivated because they do not exist. -> [Help 1]
...
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MissingProfilesException
Maven 3 warns and goes on, so a typo such as -Pdeploy-prod silently builds without the profile. Maven 4 (release candidate) fails the
build, and a profile that may legitimately be missing is prefixed with ?: -P '?nope' printed
The requested optional profiles [nope] could not be activated or deactivated because they do not exist. as an INFO message and
continued. On Maven 3.9.16 and 3.10.0 the ? is not recognized, and -P '?nope' still produced the warning, with the id ?nope. The
prefix also works for deactivation in Maven 4: -P '!?nope,!os-arch' printed the same INFO message for nope and
deactivated os-arch.
Profile order and precedence
When several profiles are active in the same POM, every element they declare overrides the POM’s own element of the same name, or extends
it if it is a collection. If two active profiles set the same thing, the one declared later in the POM wins; the order of the ids
in -P and the order in which they were activated make no difference. A scratch POM had a property region and a repository in the
POM, and the profiles first and second, each setting region and adding a repository. With -Psecond,first:
$ mvn -q help:evaluate -Dexpression=region -DforceStdout -Psecond,first
from-second
and the repositories in the effective POM (mvn help:effective-pom -Psecond,first), before the ones from the super POM, were:
second-repo
first-repo
global-repo
The repository of the last declared profile comes first, then those of earlier profiles, and the POM’s own come last. The order of a
repository list matters, since Maven asks them in that order (see
Repositories, Mirrors & Repository Managers).
Profiles of settings.xml are applied after the POM ones, which is why the settings profile won in the earlier example.
Profile ids are matched per project. In a multi-module build, -Pfast was active in the parent and in both children, and the child b
that declared its own fast profile had two: its own and the one inherited from the parent:
Active Profiles for Project 'io.tidepool:multi:pom:1':
- fast (source: io.tidepool:multi:1)
Active Profiles for Project 'io.tidepool:a:jar:1':
- fast (source: io.tidepool:multi:1)
Active Profiles for Project 'io.tidepool:b:jar:1':
- fast (source: io.tidepool:b:1)
- fast (source: io.tidepool:multi:1)
Implicit activation, in contrast, looks only at the profile’s own project: a JDK profile of the parent is evaluated for each child independently.
Seeing what is active
The commands that answer "which profile is on?":
mvn help:active-profiles # per project, with the source of each profile
mvn help:effective-pom -Pfast # the POM after profile injection and inheritance
mvn help:effective-settings # the merged settings.xml, including its profiles
The first is the quick check, and the second shows the result of the profiles. Both are part of the Help plugin; see Plugins & Their Configuration for plugin goals invoked from the command line.
Maven 4: condition-based activation
Maven 4 (release candidate) adds a condition element to activation. It holds an expression, so a profile can be activated by logic
that the older jdk, os, property and file elements cannot say, including "this or that" and string tests. The POM must declare
model version 4.1.0 (the same profile in a 4.0.0 POM failed with the model contains elements that require a model version of 4.1.0):
<project xmlns="http://maven.apache.org/POM/4.1.0" root="true">
<modelVersion>4.1.0</modelVersion>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-cond</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<profiles>
<profile>
<id>csv-present</id>
<activation>
<condition>exists("${project.basedir}/data/stations.csv")</condition>
</activation>
</profile>
<profile>
<id>pier-and-new-jdk</id>
<activation>
<condition>${tide.site} == 'pier' && inrange(${java.specification.version}, '[21,)')</condition>
</activation>
</profile>
<profile>
<id>site-contains</id>
<activation>
<condition>contains(lower(${tide.site}), 'pie')</condition>
</activation>
</profile>
</profiles>
</project>
mvn help:active-profiles on Maven 4.0.0-rc-7 listed csv-present only, and with -Dtide.site=pier it listed all three; with
-Dtide.site=Pier only csv-present and site-contains. The == comparison is case sensitive, and || also works: ${tide.site} == 'pier' || ${tide.site} == 'dock' was active for
-Dtide.site=dock and inactive for quay. The condition language is documented only in the issue MNG-8286, so the functions that follow
were read from the 4.0.0-rc-7 implementation, and only those used above were run: length, upper, lower, substring, indexOf,
contains, matches, not, if, exists, missing and inrange (which takes a version and a range). The implementation also reads
${project.basedir}, ${project.rootDirectory}, ${project.artifactId} and ${project.packaging}, besides system and user
properties. A bare name that is not a variable, such as java.version without ${}, fails with
Error parsing profile activation condition: Unknown variable: java.version. The other Maven 4 changes are on
What’s New in Maven 4 and
Maven 4: POM Model & Consumer POM.
Building for different environments
The classic use of profiles is one codebase deployed to several environments (dev, test, prod) with different configuration. The
Maven guide shows the idea with a profile per environment that changes the resource directory or a filtered value, and names the
profiles after the property that selects them (env-dev, env-test, env-prod for -Denv=dev, and so on), so the command line is
easy to guess. The tidepool example earlier is that approach with a single property.
The guide itself says the technique is for simple cases, "a single file or a small set of files that vary for each environment". The
fundamental problem is that it builds a different artifact for each environment: the jar tested in test is not the one shipped
to prod. The usual alternatives, which keep one artifact:
-
external configuration read at run time: environment variables, system properties, a configuration file next to the application, or a configuration service;
-
a separate artifact for configuration, versioned and deployed on its own;
-
profiles that select tooling (the repository to deploy to, a quality gate) but not the content of the artifact.
A profile that changes only how the build runs, such as one that adds a plugin for coverage, is a good fit. A profile that changes what is in the artifact deserves suspicion.
Pitfalls
The Maven documentation names two main problem areas. Both break the principle that the same POM builds the same way everywhere:
-
External properties. A property defined in a profile of
settings.xml, for exampleappserver.home, and used in the POM (<appserverHome>${appserver.home}</appserverHome>) makes the build work on your machine, and fail on a colleague’s machine that has no such profile, with a missing or literal${appserver.home}value. Define the profile in the POM, or give the property a default in the POMproperties, as the tidepool example did forstation.url. -
Incomplete specification of a natural set of profiles. Profiles for the environments
devandprodand a build fortestthat matches none of them. Define the whole set, with a default, and name the profiles after the property that selects them.
Other traps found on this page:
-
activeByDefaultis switched off by any other active profile of the same POM, including implicitly activated ones. -
A missing profile id is only a warning in Maven 3, and an error in Maven 4 unless it is prefixed with
?. -
A profile cannot change
sourceDirectory,outputDirectoryand other parts ofbuild, and a settings profile cannot addbuildcontent at all. -
Activation properties see system and user properties, not the properties of the POM; the file activation does not interpolate POM properties either.
-
The same profile id in a parent and a child produces two active profiles in the child.
-
A profile can silently change the dependencies of a project, and therefore its dependency tree (see Dependency Analysis & Convergence).
-
Releases are better done without a profile that alters the code, since a release must be reproducible (see Releasing and CI-Friendly Versions).
The Enforcer plugin has rules that can fail a build early, for example when a property that a profile should have set is missing (see Enforcer).
The same idea in Ant and Gradle
Ant has no profile concept. Its closest equivalents are properties given on the command line with -D, which win over the build file,
and the if and unless attributes of targets (see Ant Properties).
Gradle has no profiles either: build logic is code, so a condition is an if in the script, a property read with providers, or a
convention plugin that applies a set of settings, and the environment-specific values come from gradle.properties files and
-P project properties (see Build Environment & Properties).
The mapping of all three tools is on
Comparing Ant, Maven & Gradle, and the move from Maven
profiles to Gradle is on
Migrating from Maven.
Summary
-
A profile is a conditional set of POM changes with an
id; it lives inpom.xml(many elements) orsettings.xml(properties and repositories only). -
Activation is by
-P,activeProfiles,jdk,os,property(including thepackagingproperty since 3.9.0),file, andactiveByDefault; several different types in one profile must all match. Maven 4 addscondition. -
-P '!id'or-P=-iddeactivates;?idmarks an optional profile in Maven 4, where an unknown profile is an error. -
The profile declared last wins; settings profiles are applied after the POM’s.
-
help:active-profilesandhelp:effective-pomshow what is in effect. -
Use profiles for how the build runs, not for what the artifact contains, and prefer runtime configuration for environments.