ZetCode

Maven profiles

last modified November 8, 2026

In this article we show how to use Maven profiles to adapt a build to different environments. We activate profiles explicitly and automatically, inspect which profiles are active, and look at the pitfalls of default profiles. The examples use Maven 3.10 and Java 25.

A profile is a named set of POM elements that is applied only when the profile is active. A profile can contain properties, dependencies, plugins and their configuration, resources, repositories, and modules. Typical uses are settings for development and production environments, builds that differ between operating systems or JDK versions, and optional steps of a build, such as code signing, that run only on a release server.

Defining profiles

Profiles are defined in the profiles section of the POM. Our application reads its configuration from a properties file. The values come from Maven properties, which two profiles set differently.

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>app</artifactId>
    <version>1.0.0</version>

    <properties>
        <maven.compiler.release>25</maven.compiler.release>
    </properties>

    <build>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>

    <profiles>
        <profile>
            <id>dev</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <properties>
                <app.env>development</app.env>
                <app.db.url>jdbc:h2:mem:shop</app.db.url>
                <app.log.level>DEBUG</app.log.level>
            </properties>
        </profile>
        <profile>
            <id>prod</id>
            <activation>
                <property>
                    <name>env</name>
                    <value>prod</value>
                </property>
            </activation>
            <properties>
                <app.env>production</app.env>
                <app.db.url>jdbc:postgresql://db.example.com/shop</app.db.url>
                <app.log.level>WARN</app.log.level>
            </properties>
        </profile>
    </profiles>
</project>

The build section turns on filtering of the resources. During the process-resources phase, Maven replaces ${...} placeholders in the resource files with property values.

<profile>
    <id>dev</id>
    <activation>
        <activeByDefault>true</activeByDefault>
    </activation>

Every profile has an id. The dev profile is active by default, so a plain mvn package uses the development settings.

<activation>
    <property>
        <name>env</name>
        <value>prod</value>
    </property>
</activation>

The prod profile is activated when the env property has the value prod, for instance with -Denv=prod.

src/main/resources/app.properties
env=${app.env}
db.url=${app.db.url}
log.level=${app.log.level}

The resource file contains the placeholders. The app. prefix of the property names avoids clashes with properties that Maven or plugins define.

src/main/java/com/example/App.java
package com.example;

import java.io.IOException;
import java.util.Properties;

public class App {

    public static void main(String[] args) throws IOException {
        var props = new Properties();

        try (var in = App.class.getResourceAsStream("/app.properties")) {
            props.load(in);
        }

        props.stringPropertyNames().stream().sorted()
                .forEach(key -> System.out.printf("%-10s %s%n", key, props.getProperty(key)));
    }
}

The application loads the properties file from the classpath and prints its entries.

Without options, the dev profile is active and the development values were written into the file.

$ mvn -q package
$ java -cp target/app-1.0.0.jar com.example.App
db.url     jdbc:h2:mem:shop
env        development
log.level  DEBUG

Activating profiles

The -P option activates profiles by their ids; several ids are separated by commas. A profile with a property activation is activated by setting the property.

Both commands activate the prod profile. When we activate a profile explicitly, the dev profile, which is active by default, is deactivated. The same happens when another profile of the POM is activated by its activation rule, as with -Denv=prod.

$ mvn -q package -P prod
$ java -cp target/app-1.0.0.jar com.example.App
db.url     jdbc:postgresql://db.example.com/shop
env        production
log.level  WARN
$ mvn -q package -Denv=prod
$ java -cp target/app-1.0.0.jar com.example.App
db.url     jdbc:postgresql://db.example.com/shop
env        production
log.level  WARN

Inspecting profiles

The Help plugin shows which profiles are active and where they come from. The active-profiles goal lists the active profiles; the all-profiles goal lists all profiles of the project with their state.

The source of a profile is the project that defines it. Profiles from the settings.xml file are reported as external. For a quick check, help:effective-pom shows the result of all active profiles merged into the POM.

$ mvn help:active-profiles -Denv=prod
Active Profiles for Project 'com.example:app:jar:1.0.0':
The following profiles are active:
 - prod (source: com.example:app:1.0.0)
...
$ mvn help:all-profiles
  Profile Id: dev (Active: true, Source: pom)
  Profile Id: prod (Active: false, Source: pom)
...

Deactivating profiles

An exclamation mark or a minus sign before the id deactivates a profile. In Bash, an argument with an exclamation mark must be quoted.

Without any of the two profiles, the properties are not defined. Maven leaves undefined placeholders in filtered resources as they are, without a warning, so the application gets ${app.env} as a value.

$ mvn -q package -P '!dev'
$ java -cp target/app-1.0.0.jar com.example.App
db.url     ${app.db.url}
env        ${app.env}
log.level  ${app.log.level}

Automatic activation

Besides properties, profiles can be activated by the JDK version, the operating system, and the existence of files. We extend the project with three such profiles. The dev and prod profiles are the same as before.

pom.xml (automatically activated profiles)
    <!-- automatically activated profiles -->
    <profile>
        <id>jdk25</id>
        <activation>
            <jdk>[25,)</jdk>
        </activation>
        <properties>
            <app.jdk.note>running on Java 25 or later</app.jdk.note>
        </properties>
    </profile>
    <profile>
        <id>unix</id>
        <activation>
            <os>
                <family>unix</family>
            </os>
        </activation>
        <properties>
            <app.script>start.sh</app.script>
        </properties>
    </profile>
    <profile>
        <id>windows</id>
        <activation>
            <os>
                <family>windows</family>
            </os>
        </activation>
        <properties>
            <app.script>start.cmd</app.script>
        </properties>
    </profile>
    <profile>
        <id>local</id>
        <activation>
            <file>
                <exists>${project.basedir}/local.properties</exists>
            </file>
        </activation>
        <properties>
            <app.db.url>jdbc:h2:file:./data/shop</app.db.url>
        </properties>
    </profile>
</profiles>

The activation rules work as follows:

When an activation has several conditions, all of them must be met.

src/main/resources/app.properties
env=${app.env}
db.url=${app.db.url}
log.level=${app.log.level}
script=${app.script}
jdk=${app.jdk.note}

The resource file now also contains the values from the new profiles.

The activeByDefault trap

We build the extended project without any options.

The jdk25 and unix profiles are active, but the dev profile is not, and its properties are missing. A profile with activeByDefault is active only if no other profile of the same POM is active, and this includes profiles activated automatically by the JDK or the operating system. Here the default profile was switched off on every Linux and macOS machine, without any warning.

$ mvn help:active-profiles
 - jdk25 (source: com.example:auto:1.0.0)
 - unix (source: com.example:auto:1.0.0)
...
$ mvn -q package
$ java -cp target/auto-1.0.0.jar com.example.App
db.url     ${app.db.url}
env        ${app.env}
jdk        running on Java 25 or later
log.level  ${app.log.level}
script     start.sh

File activation

The local profile is active when the project directory contains a local.properties file. Such a file is typically listed in .gitignore, so each developer can switch to local settings without changing the POM.

The local profile overrides the database URL. The other development values are still missing because of the activeByDefault problem.

$ touch local.properties
$ mvn help:active-profiles
 - jdk25 (source: com.example:auto:1.0.0)
 - unix (source: com.example:auto:1.0.0)
 - local (source: com.example:auto:1.0.0)
...
$ mvn -q package
$ java -cp target/auto-1.0.0.jar com.example.App
db.url     jdbc:h2:file:./data/shop
env        ${app.env}
jdk        running on Java 25 or later
log.level  ${app.log.level}
script     start.sh

A reliable default profile

A better way to make a profile active by default is to activate it when a property is missing. A property name starting with an exclamation mark means that the property must not be defined.

pom.xml (dev profile)
<id>dev</id>
<activation>
    <property>
        <name>!env</name>
    </property>
</activation>

The dev profile is now active whenever the env property is not set, regardless of other active profiles. With -Denv=prod, the prod profile is activated and dev is not.

Now all the expected profiles are active and all values are filled in.

$ mvn help:active-profiles
 - dev (source: com.example:fixed:1.0.0)
 - jdk25 (source: com.example:fixed:1.0.0)
 - unix (source: com.example:fixed:1.0.0)
...
$ mvn -q package
$ java -cp target/fixed-1.0.0.jar com.example.App
db.url     jdbc:h2:mem:shop
env        development
jdk        running on Java 25 or later
log.level  DEBUG
script     start.sh

Plugins in profiles

A profile can also add or configure plugins. Our prod profile adds an entry to the JAR manifest.

pom.xml (prod profile)
<id>prod</id>
<activation>
    <property>
        <name>env</name>
        <value>prod</value>
    </property>
</activation>
<properties>
    <app.env>production</app.env>
    <app.db.url>jdbc:postgresql://db.example.com/shop</app.db.url>
    <app.log.level>WARN</app.log.level>
</properties>
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <version>3.5.1</version>
            <configuration>
                <archive>
                    <manifestEntries>
                        <Build-Environment>production</Build-Environment>
                    </manifestEntries>
                </archive>
            </configuration>
        </plugin>
    </plugins>
</build>

The build element of a profile is merged into the build element of the POM. The JAR plugin gets the manifestEntries configuration only in production builds.

The Build-Environment entry appears in the manifest of the production build.

$ mvn -q package -Denv=prod
$ unzip -p target/fixed-1.0.0.jar META-INF/MANIFEST.MF
Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.5.1
Java-Version: 25
Build-Jdk-Spec: 25
Build-Environment: production

Several active profiles

If several active profiles set the same property, the profile declared later in the POM wins. With -P prod, but without the env property, both dev and prod are active.

The prod profile is declared after dev, so its values are used. Relying on the order of profiles is fragile; it is better to design profiles so that conflicting ones are never active together. Here, using -Denv=prod instead of -P prod does that.

$ mvn help:active-profiles -P prod
 - dev (source: com.example:fixed:1.0.0)
 - prod (source: com.example:fixed:1.0.0)
 - jdk25 (source: com.example:fixed:1.0.0)
 - unix (source: com.example:fixed:1.0.0)
...
$ mvn -q package -P prod
$ java -cp target/fixed-1.0.0.jar com.example.App
db.url     jdbc:postgresql://db.example.com/shop
env        production
jdk        running on Java 25 or later
log.level  WARN
script     start.sh

Profiles in settings.xml

Profiles can also be defined in the user's ~/.m2/settings.xml file or in a file given with the -s option. Such profiles can contain only properties, repositories, and plugin repositories, and are typically used for machine-specific settings, such as the URL of a company repository.

settings.xml
<settings>
    <profiles>
        <profile>
            <id>verbose-logging</id>
            <properties>
                <app.log.level>TRACE</app.log.level>
            </properties>
        </profile>
    </profiles>

    <activeProfiles>
        <activeProfile>verbose-logging</activeProfile>
    </activeProfiles>
</settings>

The activeProfiles element activates the profile for every build. Profiles in settings.xml can also have activation rules.

The profile from the settings file is reported with the external source. Its property takes precedence over the same property from the POM profile, so the log level is TRACE.

$ mvn help:active-profiles -s ../settings.xml
 - verbose-logging (source: external)
 - dev (source: com.example:fixed:1.0.0)
 - jdk25 (source: com.example:fixed:1.0.0)
 - unix (source: com.example:fixed:1.0.0)
...
$ mvn -q package -s ../settings.xml
$ java -cp target/fixed-1.0.0.jar com.example.App
db.url     jdbc:h2:mem:shop
env        development
jdk        running on Java 25 or later
log.level  TRACE
script     start.sh

Missing profiles

A profile can be deactivated even if it was activated automatically. If we request a profile that does not exist, Maven only prints a warning, so a typo in a profile name does not stop the build.

The warning is easy to miss in a long build log. In scripts and CI pipelines, it pays to check the active profiles with help:active-profiles once when setting up the build.

$ mvn validate -P '!unix,nosuch'
[WARNING] The requested profile "nosuch" could not be activated because it does not exist.
[INFO] BUILD SUCCESS
$ mvn help:active-profiles -P '!unix'
 - dev (source: com.example:fixed:1.0.0)
 - jdk25 (source: com.example:fixed:1.0.0)
...

Best practices

Source

Introduction to build profiles

In this article we have used Maven profiles to adapt a build to different environments and shown how they are activated.

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.