Setting up your local dev environment and starting Jahia
This page is reference documentation rather than a tutorial. If you are new to Jahia development, we recommend you start with one of our tutorials:
- Getting started with Java development for Jahia
- Getting started with front-end development for Jahia
This page covers all technical details to set up a Jahia instance, for local development and for continuous integration, tailored for the specific needs of your projects. Once this development environment is set up, share it with your team instead of having one environment per developer. Keep the environment in the repository, and commit a short document that states how to start it. The rest of this page describes what to put in both.
How to organise the repositories of a project
Many Jahia projects keep one repository per module. That layout works, but a project of more than one module pays two costs:
-
A change that crosses modules costs several pull requests. One feature that touches three modules becomes three pull requests, in three repositories, reviewed and released separately. If production deployments are done manually, this may create an implicit order of deployment, and crash the instance if the order is wrong.
-
The project itself has no home. Project-level issues and files have no natural place to live. They either sit in the repository considered "the main one", or are copied across all of them.
To avoid those costs, we recommend you develop following the monorepo pattern: one repository for all modules of the project. Jahia modules live in subdirectories of the monorepo, and the repository root contains documentation, build orchestration files (e.g. an additional pom.xml to build all modules from the root) and other project-level resources.
If that is not possible, create a <project>-meta repository as a first step. If you cannot merge the module repositories now, create one repository for the project itself. That repository holds the development environment, the provisioning manifests, the developer documentation and optionally git submodules to clone all children repositories. It's a step towards a monorepo, and gives a place to put the files we'll create in the next sections.
Set up your editor
We are not prescriptive about the editor you use, but we know that Jahia development is smooth in IntelliJ IDEA and Visual Studio Code.
Install the development tools
A Jahia project needs a JDK, Maven, and sometimes Node.js and Yarn, each at a set version to ensure compatibility (e.g. Java 17). Tools installed at the machine level give every developer a different set of versions, and a version difference can cause build and runtime failures. CI environments are not spared and it's hard to keep the versions in sync with the developers' machines.
You can solve this issue the way you want, but we recommend you use mise and have a mise.toml file at the root of your monorepo, for example:
[tools]
java = "temurin-17"
maven = "3"
node = "26"
yarn = "4.18"
Every developer and CI runner then runs one command inside the repository, and gets the four (or more) tools at the expected versions:
mise install
On GitHub Actions, use the idiomatic jdx/mise-action to install mise and the tools it manages.
Running Jahia requires* an OCI runtime like Docker. Follow the official instructions to install Docker on your machine. CI runners usually have Docker installed, but check the documentation of your CI provider to be sure.
* You can run Jahia on bare metal, it's a Java application, but you'll lose the benefits of a reproducible environment. The rest of this page assumes you run Jahia in Docker.
Starting Jahia
We'll start Jahia through Docker, using a declarative approach: Docker Compose and Jahia Provisioning.
The Compose file describes the containers to run, and the provisioning manifest puts the Jahia instance in the correct state to resume development or run the end-to-end tests.
Those two files replace the manual steps that a new developer cannot guess. Keep them at the root of the repository for easier discovery, and commit them so that they evolve with the project.
Choose the base image
- You develop a template set:
jahia/jahia-ee - You develop a UI extension and want a demo website:
jahia/jahia-discovery
Jahia images run for 30 days without a license, and a free development license extends that. Install it from the administration UI of the running instance. If the instance must start with a license, set the JAHIA_LICENSE environment variable to the base64-encoded content of the license file.
We recommend you pin the version of the image to the same version deployed in production, to avoid (bad) surprises during deployment.
Choose the database
Jahia runs on five database engines: MariaDB, MySQL, PostgreSQL, Microsoft SQL Server and Oracle. Prerequisites and system requirements describes each one, and the supported stack states which versions your Jahia version supports, and until when. Jahia runs its own integration tests against every engine and version on that list. Use the engine and the version of your production instance, for the same reason you pin the image.
The Compose file below runs MariaDB. To run another engine, replace the db service with the official image of that engine. Then change three variables on the Jahia service: DB_VENDOR, DB_HOST and DB_PORT. The image turns DB_VENDOR into a JDBC URL, and each engine has its own default port:
| Engine | DB_VENDOR | Default DB_PORT |
|---|---|---|
| MariaDB | mariadb | 3306 |
| MySQL | mysql | 3306 |
| PostgreSQL | postgresql | 5432 |
| Microsoft SQL Server | mssql | 1433 |
| Oracle | oracle | 1521 |
DB_NAME, DB_USER and DB_PASS keep their meaning, and must match what the database container creates. Configuring the Jahia image documents those variables, and every other variable the image reads.
The image builds the JDBC URL only when you leave DB_URL empty. Set DB_URL to write it yourself, with the connection parameters your engine needs.
An instance with no database service at all falls back to a Derby embedded in its own data volume. That fallback is what makes a single docker run enough for the tutorials. It is not a recommended engine, so keep it for a throwaway instance.
Write the Compose file
This file starts a Jahia and a MariaDB, keeps the data in two volumes, and reads the provisioning manifest from the repository (./provisioning/bootstrap.yaml):
# docker-compose.yml
services:
jahia:
image: jahia/jahia-ee:8.2.3.2
depends_on:
db:
condition: service_healthy
ports:
- '8080:8080' # HTTP
- '8000:8000' # Java debugger
- '8101:8101' # Karaf SSH shell
environment:
DB_VENDOR: mariadb
DB_HOST: db
DB_NAME: jahia
DB_USER: jahia
DB_PASS: jahia
SUPER_USER_PASSWORD: root1234
JPDA: 'true'
# The file: scheme is required, Jahia does not resolve a bare path
EXECUTE_PROVISIONING_SCRIPT: 'file:/opt/provisioning/bootstrap.yaml'
volumes:
- jahia-data:/var/jahia
- ./provisioning:/opt/provisioning:ro
healthcheck:
test: ['CMD-SHELL', 'curl -sf http://localhost:8080/cms/login || exit 1']
interval: 10s
timeout: 5s
retries: 60
start_period: 60s
db:
image: mariadb:10-focal
command: --max_allowed_packet=234217728 --transaction-isolation=READ-UNCOMMITTED
# Not exposed but you can add ports: ['3306:3306'] to make it accessible
environment:
MARIADB_ROOT_PASSWORD: root
MARIADB_DATABASE: jahia
MARIADB_USER: jahia
MARIADB_PASSWORD: jahia
volumes:
- db-data:/var/lib/mysql
healthcheck:
test: ['CMD-SHELL', 'MYSQL_PWD=root healthcheck.sh --connect --innodb_initialized']
interval: 10s
retries: 12
start_period: 30s
# Preserve data across restarts
volumes:
db-data:
jahia-data:
You can refine this file over time to better reflect the production environment or follow the technical evolution of the project. Please refer to the official Docker Compose documentation to see examples and options.
Write the provisioning manifest
The goal of the manifest is to turn a fresh Jahia into the instance you need for development or testing. It installs the modules your project needs, and it imports or creates the site you work on.
The manifest is a list of operations that Jahia runs in order, the first time the container starts. The Compose file above points EXECUTE_PROVISIONING_SCRIPT at it. Jahia runs it again whenever you recreate the container, and not when you restart an existing one.
Here is a sample provisioning manifest with common operations:
# provisioning/bootstrap.yaml
# Register a private corporate Maven repository
- addMavenRepository: 'https://example.com/nexus'
# The modules the project needs and does not build itself. Pin every version
- installOrUpgradeModule: 'mvn:org.jahia.modules/javascript-modules-engine/1.2.0'
# Import a staging website from an export
- importSite: 'file:/opt/provisioning/export.zip'
# Create an empty site from a template set, if you don't have an export.
- createSite:
siteKey: 'myproject'
templateSet: 'my-template-set'
locale: 'en'
serverName: 'localhost'
These operations cover most project needs but the provisioning API can do more. Please refer to the Provisioning reference for a complete list of operations and options. If the built-in operations are not enough, a manifest can go as far as running a Groovy script at startup.
Start it
Run your compose stack with a single command in the root of the repository, and wait for the containers to be healthy:
docker compose up --wait
Once the command returns, the editing interface is available at http://localhost:8080. You can log in as root with the password root1234, unless you changed it in the configuration.
Jahia answers on the HTTP port before it runs the manifest, so the modules and the site appear a moment later. The image installs the manifest under the name 999-docker-provisioning.yaml, and Jahia logs one line when it finishes:
Execution of script 999-docker-provisioning.yaml : .installed took 12345 ms
That line says the manifest ran to the end. An operation that failed inside it writes a line of its own, and leaves the verdict of the manifest untouched:
Execution of script index-site.graphql : .failed took 60 ms
You can monitor the logs of the Jahia container through the Docker UI or in console with the docker compose logs command. For example, to see the last 10 lines of the Jahia container logs and follow new log entries, run:
docker compose logs --tail 10 --follow jahia
The same environment runs in a pipeline, whichever continuous integration system you use. Wait for that log line before the tests start, otherwise they run against an instance that has neither the modules nor the site:
docker compose up --wait
# Wait for the manifest, and fail the job if it never finishes
timeout 300 sh -c 'until docker compose logs jahia | grep -q "999-docker-provisioning.yaml : .installed"; do sleep 5; done'
# Fail the job on an operation that failed inside the manifest
docker compose logs jahia | grep ' : .failed' && exit 1
# build the modules, deploy them, then run the tests against http://localhost:8080
docker compose down --volumes
Adding Elasticsearch
Augmented Search and jCustomer both keep their data in Elasticsearch, and one instance serves both. They run on the same major version, and they name their indices differently. The two sections that follow point at the service below, so the stack carries one Elasticsearch and not two.
Build the image
Augmented Search creates its indices with analyzers that live in Elasticsearch plugins, so stock Elasticsearch is not enough. Index creation fails, and the indexation job dies with failed to find tokenizer under name [icu_tokenizer]. Build the image with the plugins instead:
# elasticsearch/Dockerfile
FROM docker.elastic.co/elasticsearch/elasticsearch:9.5.3
# analysis-icu is always required. The other two serve Polish and Japanese sites
RUN elasticsearch-plugin install --batch analysis-icu analysis-stempel analysis-kuromoji
9.1.3 is the lowest version that works, because elasticsearch-connector 4.x carries the elasticsearch-java 9.1.3 client. That client talks to a server of its own version or of a later one, never an earlier one. The Dockerfile above pins a later 9.x. jCustomer 3.0 asks for Elasticsearch 9 or above, which the same range satisfies.
If you run jCustomer without Augmented Search, the plugins are of no use and the stock image is enough.
Add the container
Add the service to the Compose file, and make Jahia wait for it:
services:
jahia:
# Merge with your existing `jahia:` block
depends_on:
db:
condition: service_healthy
elasticsearch:
condition: service_healthy
elasticsearch:
build: ./elasticsearch
mem_limit: 2g
ports:
- '9200:9200'
volumes:
- es-data:/usr/share/elasticsearch/data
environment:
discovery.type: single-node
# Local development only. A deployed Elasticsearch needs TLS and authentication
xpack.security.enabled: 'false'
xpack.security.http.ssl.enabled: 'false'
ES_JAVA_OPTS: '-Xms1g -Xmx1g'
healthcheck:
test: ['CMD-SHELL', 'curl -sf http://localhost:9200/_cluster/health || exit 1']
interval: 10s
timeout: 5s
retries: 30
start_period: 30s
# Without this volume, recreating the container empties the search index
volumes:
es-data:
Adding Augmented Search
Augmented Search indexes the content of your sites in Elasticsearch. It uses the service of the section above. It adds four operations to the manifest: the app-store repository, the connector configuration, the modules, and a first indexation.
Augmented Search is licensed separately. Without that entitlement in your license, the augmented-search bundle installs and never starts, which reads like a deployment failure in the logs.
Add it to the manifest
# The Jahia public app store serves these bundles, and needs no credentials
- addMavenRepository: 'https://devtools.jahia.com/nexus/content/repositories/jahia-public-app-store@id=JahiaStore'
# Configure the connector BEFORE installing it. Jahia keeps configuration
# independently of the bundle, so the connector starts already pointed at Elasticsearch
- editConfiguration: 'org.jahia.modules.elasticsearchConnector'
properties:
elasticsearchConnector.host: 'elasticsearch'
elasticsearchConnector.port: '9200'
elasticsearchConnector.useXPackSecurity: 'false'
elasticsearchConnector.useEncryption: 'false'
- installOrUpgradeModule:
- 'mvn:org.jahia.modules/elasticsearch-connector/4.1.0'
- 'mvn:org.jahia.modules/augmented-search/4.1.0'
autoStart: true
# Register the site with Augmented Search and index it. The sleep gives the
# connector time to reach Elasticsearch
- sleep: 15000
- executeScript: 'file:/opt/provisioning/index-site.graphql'
The last operation runs a GraphQL mutation, which you keep next to the manifest:
# provisioning/index-site.graphql
mutation {
admin {
search {
addSite(siteKey: "myproject")
startIndex(siteKeys: ["myproject"]) {
jobs {
id
}
}
}
}
}
Indexation is asynchronous, and the mutation answers with a job id rather than a result. It answers successfully even when the job then fails, so the response proves nothing. The indices are the end state to check:
curl 'http://localhost:9200/_cat/indices/jahia_as*?v&h=index,docs.count'
No index, or a document count of zero, means the indexation did not run. A search can answer nothing while that count climbs, because Augmented Search moves its read alias onto the new index when the job ends. You may find the root cause in the logs of the Jahia container, or in the logs of the Elasticsearch container.
Adding jExperience and jCustomer
jExperience personalizes content and reports on what visitors do. It stores that data in jCustomer, which is Jahia's distribution of Apache Unomi, and jCustomer stores it in the Elasticsearch of the section above. jExperience 4.2 requires Apache Unomi 3.0, which is what jCustomer 3.0 carries.
jCustomer accepts the events of a third-party provider by IP address, so Jahia needs a fixed address on the Compose network.
Add the containers
services:
jahia:
# Merge with your existing `jahia:` block
networks:
default:
ipv4_address: 172.16.1.100
jcustomer:
image: jahia/jcustomer:3.0.0
depends_on:
elasticsearch:
condition: service_healthy
ports:
- '8181:8181' # HTTP
- '9443:9443' # HTTPS
- '8102:8102' # Karaf SSH shell
environment:
UNOMI_ELASTICSEARCH_ADDRESSES: elasticsearch:9200
UNOMI_CLUSTER_PUBLIC_ADDRESS: http://localhost:8181
UNOMI_CLUSTER_INTERNAL_ADDRESS: https://jcustomer:9443
# The address given to Jahia above, the only sender these events are accepted from
UNOMI_THIRDPARTY_PROVIDER1_IPADDRESSES: 172.16.1.100
# Must hold the same value as jexperience.jCustomerKey in the manifest below
UNOMI_THIRDPARTY_PROVIDER1_KEY: 670c26d1cc413346c3b2fd9ce65dab41
UNOMI_THIRDPARTY_PROVIDER1_ALLOWEDEVENTS: login,updateProperties
UNOMI_ROOT_PASSWORD: karaf
# The image has no healthcheck of its own, so without this block `up --wait`
# returns before jCustomer answers
healthcheck:
test: ['CMD-SHELL', 'curl -sfk -u karaf:karaf https://localhost:9443/cxs/cluster || exit 1']
interval: 10s
timeout: 5s
retries: 60
start_period: 60s
# The fixed address above needs a subnet to belong to
networks:
default:
ipam:
config:
- subnet: 172.16.1.0/24
Add it to the manifest
# jExperience 4.2 requires jcontent >= 3.7.0
- installOrUpgradeModule:
- 'mvn:org.jahia.modules/jcontent/3.7.1'
- 'mvn:org.jahia.modules/jexperience/4.2.1'
autoStart: true
- editConfiguration: 'org.jahia.modules.jexperience.settings'
configIdentifier: 'global'
properties:
jexperience.jCustomerURL: 'https://jcustomer:9443'
jexperience.jCustomerUsername: 'karaf'
jexperience.jCustomerPassword: 'karaf'
jexperience.jCustomerTrustAllCertificates: 'true'
jexperience.jCustomerUsePublicAddressesForAdmin: 'false'
jexperience.jCustomerKey: '670c26d1cc413346c3b2fd9ce65dab41'
# jExperience works on the sites it is enabled on, and on no others
- enable: 'jexperience'
site: 'myproject'
jCustomerKey is the shared secret jExperience sends to jCustomer, in the X-Unomi-Peer header of every call. The value above is the default of the jCustomer image, and both sides have to hold the same one, so change UNOMI_THIRDPARTY_PROVIDER1_KEY and jexperience.jCustomerKey together. That default, and the karaf credentials, belong on your machine and nowhere else.
Once the containers are up, jExperience is a tab in the vertical bar of jContent. A missing tab means the module is not enabled on the site. A tab that reports no connection means Jahia cannot reach jCustomer at the URL in the configuration, and the Jahia logs name the reason.
The jexperience-dashboards module and a Kibana container add the analytics dashboards on top of this stack. Building a feedback form covers them, from the events a component sends to a dashboard packaged in a module.
Building and deploying
This is more specific to each project than this generic page can describe. As a rule of thumb, build a package that contains a pom.xml file with mvn clean verify, and a package that contains a package.json file with yarn install && yarn build. The build writes the artifact to a target or dist directory, and the provisioning API deploys that artifact.
To deploy to Jahia, Java modules have access to mvn jahia:deploy, and JavaScript modules ship with a deploy script, accessible through yarn jahia-deploy.
Under the hood, both scripts post the package to the provisioning API. You can do the same with curl:
curl -X POST -u root:root1234 http://localhost:8080/modules/api/provisioning \
-F 'script=[{"installOrUpgradeBundle":"package.tgz","ignoreChecks":true}]' -F 'file=@./dist/package.tgz'
Update package.tgz and ./dist/package.tgz to match the name and path of the artifact you built. The ignoreChecks option is optional. It skips the CND breaking-change protection, which is useful in development when you iterate on the CND of a module.
A successful deployment does not mean the module is correctly running afterwards. Monitor the logs of the Jahia container to see if the module started.
Repair your local environment
Change a port or a mounted directory
Docker fixes the ports and the mounts when it creates a container, and neither can change on a container that already exists. Edit the Compose file, then apply it:
docker compose up -d
Reset the password of root
Change SUPER_USER_PASSWORD, then recreate the container with docker compose up -d --force-recreate jahia. A restart is not enough, Jahia reads that variable when the container is created.
Start again from an empty Jahia
docker compose down --volumes
docker compose up --wait
down --volumes deletes every volume, so the site, the deployed modules and the search index are gone. The provisioning manifest then rebuilds the instance, which is what makes this pair of commands safe to run. Without a manifest, the same commands cost you a morning.
A module Jahia refuses to install
Four refusals carry a clear cause:
Skipping installation of <jar>, a more recent version is already installed. Jahia does not replace a module with an older version, and the request still answers with a success. Uninstall the newer version with- uninstallModule: "<symbolic-name>/<version>", then install the version you want.Unresolved requirement: Import-Package: <package>. No installed bundle exports a package this module imports. Install the module that provides the package, and add it to the manifest.Bundle <module> has unresolved dependency <other module> and won't be started. The module names<other module>in itsjahia-depends, and that module is not installed. The install itself answers with a success, and the module stays installed and stopped. Add the named module to the manifest, before the one that depends on it.Invalid license check, and the bundle stops itself a few milliseconds later. The signature of the module no longer matches its version. A signature covers onemajor.minorline, so a move to another line invalidates it. Ask the owner of the module for a build that carries a valid signature.
Differences between your machine and the pipeline
Every item below is a difference between two environments, and each one disappears when the environment is a file in the repository.
- The toolchain. Compare the JDK, Node.js and Yarn versions the pipeline installs with the versions on your machine. A
mise.tomlused on both sides removes this difference. - The modules. You installed modules by hand over weeks. A pipeline installs only what the provisioning manifest and the module dependencies declare, so add the missing module to the manifest.
- The Maven artifacts. A snapshot you built on your machine sits in
~/.m2and is published nowhere, so the pipeline cannot resolve it. - The content. A pipeline starts from an empty instance. A test that reads a site somebody created by hand passes only on that machine, so import the site from the manifest.
- The Jahia version and the operating mode. Compare the image tag the pipeline uses with the tag in your Compose file, and read
OPERATING_MODEin both. The Jahia images start in development mode, and a production instance answers a render error differently.