Building a Universal Messenger Docker image
This tutorial walks you through building a Docker image for the Universal Messenger (UM) from scratch, using the UM Kickstarter as the foundation. It is aimed at integrators and customer development teams who already have UM experience and want to run it as a container — either on a single host or as part of a larger orchestration platform.
By the end of this guide you will have:
a working Kickstarter project under version control;
a reproducible Docker image built from that project;
a running UM container (optionally with a local database via Docker Compose);
a clear understanding of what each Docker-related file in the project does and how to customise it for different environments.
1. Prerequisites
This tutorial assumes a Linux or macOS workstation.
Make sure the following tools are installed on your machine:
Oracle JDK or OpenJDK 21 or newer. UM runs on Tomcat 10+ (Jakarta EE) and requires Java 21; earlier versions are not supported by UM 7.6x.
Gradle ≥ 8.5 and Groovy 4. The easiest way to manage both is SDKMAN.
Git command-line tools.
Docker 20.10+ with BuildKit enabled (the default on current engines) and Docker Compose v2 for the local stack described later.
An editor or IDE of your choice — Pinuts recommends Visual Studio Code.
You will also need two things that only Pinuts can provide:
Credentials for the Pinuts Maven repository. These are required to resolve UM artefacts during the Gradle build.
A development UM licence file (
cmsbs.license).
2. What the UM Kickstarter is
The UM Kickstarter is a Gradle-based project scaffold provided by Pinuts. It turns an empty directory into a fully functional UM installation and adds a set of Gradle tasks for the operations you need day-to-day — installing, running, and, relevant for this tutorial, packaging UM as a Docker image.
A Kickstarter project typically contains:
a
build.gradledeclaring the UM version and any project-specific customisations;an
env/directory with one sub-directory per environment (e.g.env/devel,env/prod), holding the licence, per-environment configuration, and other files that the Gradle tasks pull in when you pass-Penv=<name>;customer-specific assets — plugins, templates, configuration fragments — that should end up in the final image.
2.1 Bootstrapping the Kickstarter
If you do not have a Kickstarter project yet, create one now. This step is covered in full on the UMDOC page Install Kickstarter; the short version is:
Clone the Pinuts devtools repository and add its
bin/directory to your$PATH:git clone https://gitext.pinuts.de/um-public/devtools.git ~/.pinuts-devtools # For bash users, a helper handles the PATH setup: (cd ~/.pinuts-devtools && make bash)Export your Pinuts Maven credentials, for example in
~/.bashrc:export PINUTS_MAVENREPO_USERNAME=<access-key-id> export PINUTS_MAVENREPO_PASSWORD=<secret-value>Scaffold a new project with the
umkickstarterscript. The optional--nameflag additionally creates a plugin skeleton under the given package:umkickstarter my-um-app --name acme.myumapp cd my-um-appDrop in your development licence at the expected path:
cp /path/to/cmsbs.license env/devel/cmsbs-conf/cmsbs.license
At this point you have a working Kickstarter project. The rest of this tutorial operates inside that project directory.
3. The Docker assets shipped with the Kickstarter
A scaffolded Kickstarter project already contains everything needed to produce a Docker image. Four files at the root of the project play a role in the build:
File | Purpose |
|---|---|
| Multi-stage build definition producing a runnable UM image. |
| Container entrypoint — applies configuration, waits for dependencies, and starts UM. |
| Excludes build output, IDE metadata, and local state from the build context. |
| Variable file consumed by the UM installer during image build. |
All four files are under version control in your project. They are treated as source, not as build output: customising the Dockerfile — adding a build argument, installing an additional OS package, changing the base image — is expected and supported.
3.1 A closer look at each file
Dockerfile. A multi-stage build. An earlier stage uses the UM installer to produce a clean UM_HOME directory tree; the final stage copies that tree into a slim runtime image (typically Amazon Corretto 21) and sets up the entrypoint. The final image exposes port 8080 by default.
.docker-entrypoint.sh. The entrypoint performs three tasks: it applies environment-driven overrides to cmsbs.properties, optionally waits for the database to become reachable (controlled by UM_STARTUP_DELAY), and finally execs the UM start script.
.dockerignore. Trims the build context. Review it if your project has large assets — anything not listed here is sent to the Docker daemon on every build.
.um.varfile. Controls the unattended UM installer inside the build. Typical entries include the target UM_HOME, admin credentials, and licence locations. You rarely edit this by hand; the Kickstarter manages it based on your Gradle properties.
4. Building the image
Build the image for a given environment. The environment name is passed via the -Penv= project property. Start with devel — that is the environment whose env/<n>/ directory received your licence in the bootstrap step, so the build has everything it needs out of the box:
gradle dockerimage -Penv=develThe resulting image is tagged as <tag>:<env>. Unless the Kickstarter is configured to use an explicit tag, <tag> defaults to the project name in lowercase. For a project named MyProject without an override, the command above produces: myproject:devel
You can verify the image is present:
docker images myprojectFor other environments (stage, prod, …), create a sibling directory under env/ with the appropriate licence and conf.d/ overrides, then repeat the build with the matching -Penv=<n>.
4.1 Running the image
A one-shot run that starts UM and maps its HTTP port to the host:
docker run --rm -p 8080:8080 myproject:develOnce the container is up, UM's backoffice is reachable at http://localhost:8080/cmsbs/. The first startup may take a minute or so while UM initialises its data directories.
This bare run has no persistent storage, no external database, and no secrets. The following sections address each of these.
5. Customising per environment
Real deployments need different configuration per stage (dev, staging, production). UM provides three complementary mechanisms; use whichever fits your setup best.
5.1 Environment variables (recommended)
Since UM 7.55.0, every setting that can be placed in cmsbs.properties can also be overridden by an environment variable. Dots in the property name may be replaced with double underscores for shell-friendliness:
docker run --rm -p 8080:8080 \
-e cmsbs__database__url='jdbc:postgresql://db:5432/um' \
-e cmsbs__database__user=um \
-e cmsbs__database__password=secret \
myproject:develThis is the cleanest approach for container deployments: a single image moves through all environments, and only the variables differ.
5.2 Environment-specific configuration files
The Kickstarter uses an env/<n>/ directory layout — the same one you already saw when dropping the licence into env/devel/cmsbs-conf/. When you build with -Penv=<n>, the contents of env/<n>/ are merged into UM_HOME/ inside the image. In particular:
env/<n>/cmsbs-conf/conf.d/project.propertiesoverrides properties for that environment. UM reads every*.propertiesfile underconf.d/as a drop-in on top of the maincmsbs.properties, so your project settings stay cleanly separated from UM's defaults;env/<n>/cmsbs-conf/cmsbs.license(and the sender / replyto CSVs) provide the licence;any other file under
env/<n>/lands at the matching relative path inUM_HOME.
Use this for settings that rarely change and that you want under version control alongside the code (e.g. localisation, tracking URLs, Tomcat tuning parameters). For anything that changes per deployment — especially secrets — prefer the mechanisms in 5.1 and 5.3.
5.3 Secrets (UM 7.60.0+)
Do not bake passwords into properties files or environment variables in production. From UM 7.60.0, any configuration key can be loaded from a file by appending the $file suffix. This pairs naturally with Docker Secrets and Kubernetes Secrets:
# Instead of this:
# cmsbs.database.password = "mySecretPassword"
# Do this:
cmsbs.database.password$file = "/run/secrets/db_password"The secret file is read at runtime; UM also masks these values in the backoffice UI. See Secrets Management in the Developer Manual for the full list of benefits and patterns.
6. Working locally with Docker Compose
For development you normally want UM plus a database in the same stack, with the Kickstarter project directory mounted into the container so changes to templates, plugins, and configuration take effect immediately.
6.1 Add the MySQL JDBC driver dependency
UM connects to MySQL (or MariaDB) via the MySQL Connector/J JDBC driver. For licensing reasons the Kickstarter does not bundle the driver, so you need to declare it as a dependency in your top-level build.gradle:
dependencies {
// ...
runtime('com.mysql:mysql-connector-j:9+') { transitive = false }
}Gradle then downloads the driver from Maven Central on the next build and places it on the UM classpath inside the image. If you skip this step, UM fails at startup with ClassNotFoundException: com.mysql.cj.jdbc.Driver.
For PostgreSQL, declare org.postgresql:postgresql:+ in the same place.
6.2 docker-compose.yml
Create a docker-compose.yml next to the Dockerfile that builds the UM image from the local project and runs a database alongside it.
services:
um:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
environment:
TZ: 'Europe/Berlin'
UM_STARTUP_DELAY: '10'
cmsbs.database.url: 'jdbc:mysql://db:3306/um?allowPublicKeyRetrieval=true'
cmsbs.database.user: 'um'
cmsbs.database.password: 'secret'
depends_on:
- db
db:
image: mariadb:10
environment:
TZ: 'Europe/Berlin'
MYSQL_DATABASE: 'um'
MYSQL_USER: 'um'
MYSQL_PASSWORD: 'secret'
MARIADB_RANDOM_ROOT_PASSWORD: 'true'
command: '--max-allowed-packet=256m'A few notes:
UM_STARTUP_DELAY: '10'gives MariaDB enough time to initialise on first start. Increase it on slower hosts if UM logs database connection errors during the first boot.MariaDB is used here because it is the simplest database for a throw-away local stack.
6.3 Naming the stack
By default, Docker Compose names the stack after the directory containing the docker-compose.yml. Override this in a .env file if needed:
COMPOSE_PROJECT_NAME=name-of-the-stack6.4 Starting and stopping
Bring the stack up with Docker Compose. The --build flag ensures the image is rebuilt when the Dockerfile or its build context changes:
docker compose up --buildAdd -d to detach and run in the background.
Stop the stack without removing containers:
docker compose stopRemove the stack entirely, including networks:
docker compose downAdd -v to docker compose down to also drop the database volume — handy when you want a pristine state.