Book a Demo

Cabinee Enterprise / Community Distribution — DevOps Deployment Runbook

1. Purpose of this document

This document is intended for:

  • DevOps engineers
  • infrastructure engineers
  • system administrators

The document describes:

  • deploying Cabinee in Docker;
  • connecting to external PostgreSQL, Kafka, Keycloak, and nginx;
  • configuring Cabinee integration with external services;
  • basic deployment validation.

This guide does not describe the installation of PostgreSQL, Kafka, Keycloak, or nginx. It is assumed that these services are already deployed and available.


2. Differences from the Community distribution installation guide

In this guide:

  • PostgreSQL is external;
  • Kafka is external;
  • Keycloak is external;
  • nginx is an external reverse proxy;
  • Cabinee is deployed as a set of application containers.

Local Docker instances of PostgreSQL, Kafka, and Keycloak are not used.

Historical identifiers. Container names, image names, file paths (generated/unibpm/…), SQL examples, Keycloak client IDs, and module names such as unibpm-camunda8-adapter may still use the unibpm-* prefix. For Cabinee Community/Enterprise deployments, use cabinee-* container images from the Kazakhstan registry where available (see the Docker Compose example below).


3. System requirements

Canonical requirements, starting sizing profiles, and scaling limits are given in the System requirements and sizing section. Values from this runbook should not be used as a standalone sizing source.

3.1 Proven project configuration

The main backend is built and run on Java 17. Current deployments use PostgreSQL 16, Keycloak 26.x, Docker Engine 24.0.7 / Docker Compose 2.21.0, and Kubernetes 1.33.x.

Process engine composition:

EngineComponentsNotes
Camunda 8orchestration cluster camunda/camunda 8.9.x, Elasticsearch 9.2.x, unibpm-camunda8-adapter, exporter inside the brokerThe exporter is built for the same version as the broker; the orchestration cluster runs on Java 21

The Elasticsearch version is tied to Camunda: 8.9 requires Elasticsearch 9.2 or newer, while 8.8 worked on 8.16+. When upgrading Camunda, indexes are not migrated directly across major Elasticsearch versions.

These versions describe proven configurations and are not universal minimum supported versions. For a specific release, consult its release notes and pin agreed image tags. Kafka 2.3.0 is found on existing stands but is not recommended for new installations; the latest tag must not be used.

3.2 Resources before deployment

Before start-up, select a starting profile in the canonical section, calculate PostgreSQL and Kafka storage separately, and set CPU/RAM limits and an explicit JVM heap. For production, recommendations must be confirmed by a load test on the customer’s processes, data, and integrations.


4. DNS and TLS requirements

4.1 DNS

Required DNS records:

uni.company.com
id.company.com

Optional:

camunda.company.com

All DNS records must point to the reverse proxy/nginx.


4.2 TLS

HTTPS is recommended.

Requirements:

  • TLS termination on nginx;
  • a correct reverse proxy;
  • valid certificates;
  • WebSocket proxying support.

Supported:

  • Let’s Encrypt;
  • corporate certificates.

4.3 Network connections and ports

Inbound connections

ComponentPortPurpose
nginx443HTTPS ingress
nginx80HTTP → HTTPS redirect

Cabinee outbound connections

SourceDestinationPortPurpose
cabinee-app (unibpm-app)PostgreSQL5432connection to the Cabinee database
cabinee-appKafka9092/9093Kafka integration
cabinee-appKeycloak443OAuth2 authentication
nginxcabinee-frontend (unibpm-frontend)80frontend reverse proxy

Process engine components:

SourceDestinationPortPurpose
cabinee-app (unibpm-app)orchestration cluster8080Camunda 8 REST API v2
camunda (exporter inside the broker)Kafka9092/9093publishing the engine record stream
camundaElasticsearch9200built-in exporter to secondary storage
unibpm-camunda8-adapterKafka9092/9093reading records and publishing events
unibpm-camunda8-adapterPostgreSQL5432businessKey mapping table

Cabinee container ports

ComponentInternal portNote
cabinee-app (unibpm-app)8099
cabinee-frontend (unibpm-frontend)80

Process engine components:

ComponentInternal portPurpose
camunda8080REST API v2, Operate, Tasklist
camunda26500Zeebe gRPC gateway
camunda9600broker management endpoints
elasticsearch9200secondary storage
unibpm-camunda8-adapter8090actuator; 8080 is used by the orchestration cluster

When using a Docker bridge network, publishing internal ports externally is usually not required.

It is recommended to expose only nginx externally. Do not expose Operate and Tasklist of the orchestration cluster externally.


Health checks

ComponentCheck
cabinee-app (unibpm-app)GET /actuator/health
camundaGET http://<host>:8080/v2/topology — 200 means the cluster has formed
elasticsearchGET http://<host>:9200/_cluster/health, status green or yellow
unibpm-camunda8-adapterGET http://<host>:8090/actuator/health; liveness and readiness probes are enabled

Additionally, after start-up verify that the exporter loaded into the broker:

docker logs <camunda-container> 2>&1 | grep -i "UniBPM exporter"

Expected lines: UniBPM exporter configured and UniBPM exporter opened. Their absence means the record stream is not going to Kafka and tickets will not change state.


5. External dependencies

5.1 PostgreSQL

Requirements

  • PostgreSQL 15+;
  • UTF-8 encoding;
  • TCP access;
  • a database for Cabinee and a schema for unibpm-camunda8-adapter.

Creating users

SQL examples below use the historical database user name unibpm. For new Cabinee installations you may create a cabinee user instead.

CREATE USER unibpm WITH PASSWORD '<strong_password>';

Creating databases

CREATE DATABASE unibpm OWNER unibpm;

There is no separate process-engine database: runtime is stored in RocksDB inside the broker, and search data is in Elasticsearch. Only a schema for unibpm-camunda8-adapter is needed, where it maintains the process-instance ↔ ticket mapping:

CREATE SCHEMA IF NOT EXISTS camunda8_adapter;

CREATE TABLE IF NOT EXISTS camunda8_adapter.business_key_mapping
(
    process_instance_key        VARCHAR(32)  NOT NULL,
    business_key                VARCHAR(255) NOT NULL,
    parent_process_instance_key VARCHAR(32),
    tenant_id                   VARCHAR(64),
    bpmn_process_id             VARCHAR(255),
    created_at                  TIMESTAMPTZ  NOT NULL DEFAULT now(),
    updated_at                  TIMESTAMPTZ  NOT NULL DEFAULT now(),
    CONSTRAINT pk_business_key_mapping PRIMARY KEY (process_instance_key)
);

CREATE INDEX IF NOT EXISTS ix_business_key_mapping_business_key
    ON camunda8_adapter.business_key_mapping (business_key);

CREATE INDEX IF NOT EXISTS ix_business_key_mapping_parent
    ON camunda8_adapter.business_key_mapping (parent_process_instance_key)
    WHERE parent_process_instance_key IS NOT NULL;

CREATE INDEX IF NOT EXISTS ix_business_key_mapping_updated_at
    ON camunda8_adapter.business_key_mapping (updated_at);

A ready script is in the repository: unibpm-app/docker/camunda8-dev/postgres/init/02c-business-key-mapping-in-unibpm-db.sql (schema inside the unibpm database) and 02-camunda8-adapter.sql (separate database variant).

The schema may live in a separate database or inside the Cabinee/unibpm database — the adapter addresses it by fully qualified name. Inside the Cabinee database it is safer: resetting Liquibase changelogs does not affect it, because the schema is not public and is not managed by Liquibase. The adapter has ddl-auto set to none, so it does not create the table itself.

Credentials are set with ADAPTER_DB_URL, ADAPTER_DB_USERNAME, ADAPTER_DB_PASSWORD; pool size is ADAPTER_DB_POOL_SIZE (default 16) and should be aligned with spring.kafka.listener.concurrency.


Privileges for the unibpm database

\c unibpm

GRANT ALL PRIVILEGES ON DATABASE unibpm TO unibpm;
GRANT USAGE, CREATE ON SCHEMA public TO unibpm;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO unibpm;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO unibpm;

Privileges for the adapter schema

\c unibpm

GRANT USAGE ON SCHEMA camunda8_adapter TO unibpm;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA camunda8_adapter TO unibpm;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA camunda8_adapter TO unibpm;

Cabinee configuration

File:

generated/unibpm/application.yaml
spring:
  datasource:
    url: jdbc:postgresql://postgres.company.com:5432/unibpm
    username: unibpm
    password: ${UNIBPM_DB_PASSWORD}

Camunda 8 Adapter configuration

Connection is set with container environment variables:

ADAPTER_DB_URL=jdbc:postgresql://postgres.company.com:5432/unibpm?currentSchema=camunda8_adapter
ADAPTER_DB_USERNAME=unibpm
ADAPTER_DB_PASSWORD=${UNIBPM_DB_PASSWORD}
ADAPTER_DB_POOL_SIZE=16

The orchestration cluster does not need PostgreSQL: runtime is in RocksDB, search data is in Elasticsearch.


5.2 Kafka

Requirements

Supported:

  • single-node Kafka;
  • multi-node Kafka cluster.

Recommended:

  • replication.factor >= 3 for production;
  • min.insync.replicas >= 2;
  • retention according to company requirements.

Topics

Main topics are created by the application automatically.

Event topics:

TopicProducerConsumer
reunico.unibpm.task.events.produnibpm-camunda8-adapterunibpm-app
reunico.unibpm.execution.events.produnibpm-camunda8-adapterunibpm-app

Engine record stream topic:

TopicProducerConsumer
reunico.unibpm.c8.recordsexporter inside the brokerunibpm-camunda8-adapter

The entire engine record stream goes through this topic, so volume is substantially higher than on the others: record types PROCESS_INSTANCE, USER_TASK, VARIABLE, INCIDENT, and JOB are exported. Set retention deliberately.

External-task topics are created when the jobs worker is enabled:

reunico.unibpm.external-task.jobs.<taskType>.prod
reunico.unibpm.external-task.results
reunico.unibpm.external-task.dlq

All topic names must match in producer and consumer configuration. A mismatch between unibpm.camunda8.change-state-topic on the adapter and camunda-kafka-import.change-state-topic on unibpm-app fails silently: the adapter publishes to a topic nobody reads, and tickets simply do not change state.

If auto topic creation is disabled, create topics manually.


Cabinee configuration

File:

generated/unibpm/application.yaml
spring:
  kafka:
    bootstrap-servers: kafka.company.com:9092

Kafka with SASL/SSL

spring:
  kafka:
    bootstrap-servers: kafka.company.com:9093
    properties:
      security.protocol: SASL_SSL
      sasl.mechanism: SCRAM-SHA-512
      sasl.jaas.config: >
        org.apache.kafka.common.security.scram.ScramLoginModule required
        username="${KAFKA_USERNAME}"
        password="${KAFKA_PASSWORD}";

5.3 Keycloak

Requirements

  • Keycloak 26.x;
  • HTTPS access.

Required clients

ClientPurpose
cabinee-app (unibpm-app)backend OAuth2 client
cabinee-frontend (unibpm-front)frontend authentication
camunda8unibpm-app access to the orchestration cluster REST API v2

A separate client is required for REST API v2. Its name is set by camunda-eight-client.oauth2-registration-id (default camunda8) and must match an entry in spring.security.oauth2.client.registration.*:

spring:
  security:
    oauth2:
      client:
        registration:
          camunda8:
            client-id: ${CAMUNDA8_CLIENT_ID}
            client-secret: ${CAMUNDA8_CLIENT_SECRET}
            authorization-grant-type: client_credentials
            provider: keycloak
            scope: openid

Two notes about this client.

The registration is resolved at request time, not at application start. If it is missing, the application starts normally and fails on the first engine call with Could not find ClientRegistration with id 'camunda8'.

The orchestration cluster itself must accept these tokens. If it is started with CAMUNDA_SECURITY_AUTHENTICATION_UNPROTECTEDAPI=true, the token is ignored — in that case a separate client is not needed; set camunda-eight-client.oauth2-enabled: false.


Client types

Recommended types:

ClientType
cabinee-app (unibpm-app)confidential
camunda8confidential
cabinee-frontend (unibpm-front)public

Client secrets

Obtaining secrets:

Realm
  → Clients
  → <client>
  → Credentials
  → Client secret

Cabinee configuration

File:

generated/unibpm/application.yaml
identity:
  keycloak:
    base-url: https://id.company.com
    realm: unibpm

OAuth2 configuration

spring:
  security:
    oauth2:
      client:
        registration:
          unibpm-keycloak:
            client-id: unibpm-app
            client-secret: ${UNIBPM_APP_SECRET}
            authorization-grant-type: client_credentials

          camunda8:
            client-id: ${CAMUNDA8_CLIENT_ID}
            client-secret: ${CAMUNDA8_CLIENT_SECRET}
            authorization-grant-type: client_credentials
            provider: unibpm-keycloak
            scope: openid

        provider:
          unibpm-keycloak:
            token-uri: https://id.company.com/realms/unibpm/protocol/openid-connect/token

5.4 nginx

Requirements

nginx is used as a reverse proxy.

Requirements:

  • HTTPS termination;
  • WebSocket support;
  • proxy headers;
  • large request support.

nginx configuration example

server {
    listen 443 ssl;
    server_name uni.company.com;

    ssl_certificate /etc/nginx/certs/fullchain.pem;
    ssl_certificate_key /etc/nginx/certs/privkey.pem;

    location / {
        proxy_pass http://unibpm-frontend:80;

        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_read_timeout 300;
        proxy_connect_timeout 300;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

6. Components to install

The following containers are deployed:

ComponentPurpose
cabinee-app (unibpm-app)backend API
cabinee-frontend (unibpm-frontend)frontend UI
camundaCamunda 8 orchestration cluster with the Cabinee exporter (unibpm-camunda8-exporter) inside the broker
elasticsearchsecondary storage for the orchestration cluster
unibpm-camunda8-adapterconverts engine records into ticket and task events

Docker Compose example

version: '3.9'

services:
  cabinee-app:
    image: cr.yandexcloud.kz/crkus9to5u5p49kovldl/cabinee-app:1.0.5
    platform: linux/amd64
    volumes:
      - ./generated/unibpm/application.yaml:/config/application.yaml:ro
    env_file:
      - .env
    ports:
      - "${BACKEND_PUBLIC_PORT:-8099}:8099"
    networks:
      - cabinee
    restart: unless-stopped

  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:${ELASTIC_VERSION}
    environment:
      discovery.type: single-node
      xpack.security.enabled: "false"
    volumes:
      - elasticsearch-data:/usr/share/elasticsearch/data
    networks:
      - cabinee
    restart: unless-stopped

  camunda:
    image: camunda/camunda:${CAMUNDA_VERSION}
    volumes:
      # Directory, not a single file: binding one file is unreliable.
      # Place the exporter JAR in ./exporters.
      - ./exporters:/usr/local/camunda/exporters:ro
    environment:
      SPRING_PROFILES_ACTIVE: broker,operate,tasklist,identity
      CAMUNDA_DATA_SECONDARYSTORAGE_TYPE: elasticsearch
      CAMUNDA_DATA_SECONDARYSTORAGE_ELASTICSEARCH_URL: http://elasticsearch:9200
      ZEEBE_BROKER_EXPORTERS_UNIBPM_CLASSNAME: com.reunico.unibpm.camunda8.exporter.UniBpmExporter
      ZEEBE_BROKER_EXPORTERS_UNIBPM_JARPATH: /usr/local/camunda/exporters/unibpm-camunda8-exporter.jar
      ZEEBE_BROKER_EXPORTERS_UNIBPM_ARGS_BOOTSTRAPSERVERS: ${KAFKA_BOOTSTRAP_SERVERS}
      ZEEBE_BROKER_EXPORTERS_UNIBPM_ARGS_TOPIC: ${C8_RECORDS_TOPIC}
    depends_on:
      - elasticsearch
    ports:
      - "${CAMUNDA_PUBLIC_PORT:-8081}:8080"
      - "26500:26500"
    networks:
      - cabinee
    restart: unless-stopped

  unibpm-camunda8-adapter:
    image: registry.reunico.com/reunico/unibpm/unibpm-camunda8-adapter:${ADAPTER_IMAGE_TAG}
    env_file:
      - .env
    depends_on:
      - camunda
    ports:
      - "${ADAPTER_PUBLIC_PORT:-8090}:8090"
    networks:
      - cabinee
    restart: unless-stopped

  cabinee-frontend:
    image: cr.yandexcloud.kz/crkus9to5u5p49kovldl/cabinee-frontend:1.0.5
    platform: linux/amd64
    depends_on:
      - cabinee-app
    ports:
      - "${FRONT_PUBLIC_PORT:-8080}:80"
    networks:
      - cabinee
    restart: unless-stopped

volumes:
  elasticsearch-data:

networks:
  cabinee:
    driver: bridge

The exporter version must match the broker minor version: it is loaded into the broker JVM and the broker will not start if the API diverges.

Unlike the Community compose, this variant has no PostgreSQL, Kafka, Keycloak, nginx, or certbot containers.


7. Environment variables

Required secrets

UNIBPM_DB_PASSWORD
UNIBPM_APP_SECRET
CAMUNDA8_CLIENT_ID
CAMUNDA8_CLIENT_SECRET
KAFKA_USERNAME
KAFKA_PASSWORD

Secrets must not be stored in Git.

Recommended options:

  • Vault;
  • Kubernetes Secrets;
  • Docker Secrets;
  • CI/CD variables.

8. Validation / Smoke Test

UI availability check

Open:

https://uni.company.com

Expected result:

  • the frontend opens;
  • redirect to Keycloak occurs;
  • authentication succeeds.

Kafka check

Check logs:

docker compose logs -f cabinee-app

Logs must not contain:

Bootstrap broker disconnected
TimeoutException
Topic not found

Keycloak check

Check token retrieval:

curl -i -X POST "https://id.company.com/realms/unibpm/protocol/openid-connect/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=client_credentials" \
  --data-urlencode "client_id=${CAMUNDA8_CLIENT_ID}" \
  --data-urlencode "client_secret=<secret>"

Expected result:

HTTP 200
access_token

PostgreSQL check

Verify that migrations complete successfully.

Logs must not contain:

permission denied
no schema has been selected
relation does not exist

Backend health endpoint check

curl http://localhost:8099/actuator/health

Expected result:

{"status":"UP"}

Orchestration cluster health check

curl http://localhost:8080/v2/topology

Expected result: HTTP 200 and a topology payload.

Health endpoints are recommended for:

  • readiness probes;
  • liveness probes;
  • monitoring;
  • external load balancer health checks.

9. Architecture diagram

Cabinee Architecture


10. Troubleshooting

401 Unauthorized

Cause:

  • incorrect client secret;
  • incorrect realm;
  • incorrect token-uri.

403 Forbidden

Cause:

  • missing role:
realm-management → realm-admin

Kafka connection errors

Cause:

  • incorrect bootstrap server;
  • firewall;
  • SASL misconfiguration.

PostgreSQL schema errors

Cause:

  • missing privileges on schema public.

11. Summary

After following this guide:

  • the Cabinee frontend is available through nginx;
  • the backend successfully connects to PostgreSQL;
  • the backend successfully connects to Kafka;
  • OAuth2 authentication goes through external Keycloak;
  • Camunda 8 is integrated (REST API v2, exporter, adapter);
  • the smoke test completes successfully.