Installation & Deployment

1. Overview

This guide deploys the Provision Portal API — the provision-api container (Spring Boot 3.5 / Java 25 backend), whose per-service files ship in the Provision Portal API folder on FT_DISK. It is a plain Compose service on the ft-network bridge, deployed from /usr/local/ft-system/. Docker Compose is the only supported deployment method for this repository: the Gradle build produces a fat jar (bootJar → provision-portal.jar) that is baked into the image, and no distribution archive is built.

The infrastructure Provision Portal API depends on — the ACS database, the Flowable workflow-engine schema, and Northbound API — is assumed to be already deployed and reachable; this guide only points the service at it. To deploy that infrastructure, see All in one server deployment or Separate server deployment — Server E.
The web interface is a separate service with its own guide — Provision Portal UI — Installation & Deployment; the one setting that links the two is covered in Connecting the UI.

Everything else in the diagram below is a prerequisite, not part of this installation:

Clients / integrators  ->  Provision Portal API  ->  MySQL / Oracle (ftacs + flowable schemas)  +  Northbound API

The image is hub.friendly-tech.com/api/provision-portal. The web interface image shares the base name under a different Harbor namespace, ui/provision-portal — confusing the two pulls the wrong container.

Provision Portal API uses the Compose project name ft-system and the directory root /usr/local/ft-system/ — the same layout as every deployment guide in the Friendly Tech portal. The Provision Portal API folder on FT_DISK carries a single-service compose.yml in that layout, for a host that runs this service on its own, plus the per-service files. On a host running the full FT System stack the same service block is already part of the stack compose.yml from the FT System MySQL Stack / FT System Oracle Stack folder — use that one, and take the shared root .env from the same folder. The docker/compose.yml in the source repository is a developer stack with its own environment split, for local development. Deployments follow the platform layout described here.

2. Prerequisites

2.1. Host Requirements

Component Minimum Recommended Notes

Docker Engine

20.10

Latest stable

The stack compose.yml uses the Compose v2 name: key and the service_healthy healthcheck condition; the single-service file on FT_DISK has neither and takes the project name from the /usr/local/ft-system directory

Docker Compose

2.0

Latest stable

Invoked as docker compose (plugin), not docker-compose

RAM

2 GB

4 GB

The JVM sizes its heap from the container memory limit; without deploy.resources.limits.memory it sizes it from the host’s total RAM

Free disk space

1 GB

10 GB

The image, plus the provision-api/logs bind mount

2.2. Required External Dependencies

These services must be installed, running, and reachable from this host before the Provision Portal API container starts. None of them are deployed by this guide.

Component Minimum Version Why It Is Needed Port Required

MySQL or Oracle — ACS schema

MySQL 8.4; Oracle 21c

Holds the ftacs schema the backend reads provisioning data and web-service users from; without it the application context fails during JDBC pool initialisation

3306 (TCP) for MySQL, 1521 (TCP) for Oracle

Yes

MySQL or Oracle — Flowable schema

MySQL 8.4; Oracle 21c

Holds the Flowable workflow-engine tables; without a pre-created schema and user, the Liquibase migrations that create those tables fail on first start and no provisioning process can run

3306 (TCP) for MySQL, 1521 (TCP) for Oracle

Yes

Northbound API

verify with product owner

Every TR-069 provisioning operation is dispatched through it; without it the REST/SOAP APIs accept requests but no device is ever provisioned

8080 on the shared bridge (the shipped NORTHBOUND_API_URL); 9880 published across hosts (NBI_HOST_PORT)

Yes

The MySQL minimum comes from application.yml (mysql.dialect: org.hibernate.dialect.MySQL8Dialect); the drivers in the image are com.mysql:mysql-connector-j:9.2.0 and com.oracle.database.jdbc:ojdbc11:23.6.0.24.10 (build.gradle). The figures in the table are the platform-wide minimums from External Components — the highest minimum any FT service states — so that a single database server satisfies the whole platform; MySQL8Dialect in application.yml establishes only that the schema is an 8.x one, not which 8.x release is supported. The ACS and Flowable schemas may live on the same database instance or on different ones — they are configured through independent JDBC URLs (application-mysql.yml / application-oracle.yml).

Provision Portal has no Hazelcast dependency and does not connect to the ACS server directly — it reaches ACS only indirectly, through Northbound API. The hazelcast-client.yaml referenced by older documentation of this service no longer applies; there is no Hazelcast configuration in the current environment file templates or in the application code.

2.3. Supported Operating Systems

Deployment Operating system

Docker Compose

Linux (recommended), macOS, or Windows with WSL2

2.4. Registry Access

Network access to the hub.friendly-tech.com Docker registry (or offline image archives — see Offline Servers).

Access to FT_DISK — Provision Portal API for compose.yml, the per-service .env, the provisioning XML files and encrypted-parameters.txt; the shared root .env comes from the FT System MySQL Stack / FT System Oracle Stack folder, or is written from the sample in Environment Configuration.

2.5. Flowable Database Setup

Provision Portal uses the Flowable workflow engine for business process management. A dedicated database schema and user are required before first startup.

The application automatically runs Liquibase migrations on startup to create and update the Flowable tables. No manual table creation is required — only the schema and user must exist.

2.5.1. MySQL

CREATE DATABASE flowable;
CREATE USER 'flowable'@'%' IDENTIFIED WITH mysql_native_password BY 'your_secure_password';
GRANT ALL PRIVILEGES ON flowable.* TO 'flowable'@'%';
FLUSH PRIVILEGES;

2.5.2. Oracle

-- Run as SYS/SYSTEM (or a user with CREATE USER/TABLESPACE).
-- If your database is CDB/PDB, switch to the target PDB first:
-- ALTER SESSION SET CONTAINER = XEPDB1;

CREATE TABLESPACE FLOWABLE_DATA
  DATAFILE SIZE 500M
  AUTOEXTEND ON NEXT 50M MAXSIZE UNLIMITED;

CREATE USER FLOWABLE IDENTIFIED BY "your_secure_password"
  DEFAULT TABLESPACE FLOWABLE_DATA
  TEMPORARY TABLESPACE "TEMP"
  QUOTA UNLIMITED ON FLOWABLE_DATA;

GRANT CREATE SESSION, CREATE TABLE, CREATE SEQUENCE, CREATE VIEW,
      CREATE PROCEDURE, CREATE TRIGGER, CREATE TYPE
  TO FLOWABLE;
Oracle pitfalls
  • ORA-00922 / ORA-00933: caused by running multiple SQL statements as one command, or unquoted passwords with special characters. In DBeaver, use Execute Script (Alt+X) or run statements one by one.

  • ORA-01950: no privileges on tablespace 'USERS': fix by setting the default tablespace: ALTER USER FLOWABLE DEFAULT TABLESPACE FLOWABLE_DATA;

  • ORA-01430: column already exists: the Flowable schema was partially created. For a fresh install: DROP USER FLOWABLE CASCADE; and re-run the setup.

On Oracle, spring.threads.virtual.enabled is already false in application-oracle.yml — the Oracle JDBC driver pins virtual threads, which defeats their purpose and can exhaust the carrier thread pool under load. The shipped provision-api/.env sets SPRING_THREADS_VIRTUAL_ENABLED=false explicitly (see Environment Configuration), which makes it visible to anyone reading the deployment and also applies it on the mysql profile.

3. Third-Party Components

Every third-party product this stack depends on, with the version it is built or verified against. Values in the Technology / Version column are read off this repository — build.gradle, Dockerfile — except the external server minimums, which come from External Components and are a platform-wide product decision rather than something this repository pins.

The table below is this stack’s own view. For the platform-wide picture — which third-party product version each published release and hot-fix was built against, across every FT service — see FT Version Control.

Component Technology / Version Purpose

Container runtime

Docker Engine 20.10+, Docker Compose 2.0+

The only supported deployment method for this repository

Base image (backend)

eclipse-temurin:25-jdk for the build stage, eclipse-temurin:25-jre for the runtime stage

Two-stage Dockerfile producing the provision-api image

Java

25

Language level and runtime (build.gradle)

Application framework

Spring Boot 3.5.7

Web layer, JPA, validation, security (build.gradle)

Workflow engine

Flowable 7.2.0

BPMN provisioning processes; owns its own database schema (build.gradle)

Relational database

MySQL 8.4 or Oracle 21c

ACS schema and the separate Flowable schema. Platform-wide minimum

MySQL driver

com.mysql:mysql-connector-j 9.2.0

JDBC access to MySQL (build.gradle)

Oracle driver

com.oracle.database.jdbc:ojdbc11 23.6.0.24.10

JDBC access to Oracle (build.gradle)

Northbound API

FT component, version tracked with the platform release

Every TR-069 provisioning operation is dispatched through it

This service has no Hazelcast and no ClickHouse dependency — see the note in Required External Dependencies.

4. Network Requirements

Connections provision-api opens outwards, and the published port clients and integrators connect to.

Destination Port Protocol Purpose

ACS database

3306 (Oracle: 1521)

TCP

provision-api JDBC connection to the ftacs schema

Flowable database

3306 (Oracle: 1521)

TCP

provision-api JDBC connection to the Flowable schema

Northbound API

8080 on the shared bridge; 9880 published across hosts

TCP

provision-api REST calls that dispatch TR-069 provisioning operations

provision-api (from clients, integrators, the web interface and Prometheus)

8091 → 8080

TCP

REST/SOAP API, Swagger UI, Actuator, /prov-portal/actuator/prometheus

provision-api also serves HTTPS on container port 8443 (server.ssl.enabled: true with a self-signed keystore inside the image, port from HTTPS_PORT in provision-api/.env) — but the stack compose.yml does not publish 8443 to the host, only 8080, and its healthcheck probes the plain-HTTP connector. If you need HTTPS access to the API from outside the container, add a "8443:8443" entry to the ports: list yourself.

For a quick connectivity check from any host:

nc -zv <target-ip> <port>

4.1. Docker Networking

provision-api joins the ft-network bridge of the ft-system Compose project, together with every other FT service on the host, and is reached there by service name (provision-api:8080).

The single-service compose.yml on FT_DISK declares extra_hosts: - "host.docker.internal:host-gateway" for provision-api; the same block in the stack compose.yml does not carry it, because there the database and Northbound API are compose services reached by name. If the database or Northbound API runs on the host machine rather than in Docker, use host.docker.internal as the hostname — it resolves on Docker Desktop out of the box and, thanks to that extra_hosts entry, on Linux as well. The host’s real IP address works too. localhost / 127.0.0.1 will not work: inside the container they point at the container itself.

5. Registry Authentication

The image is pulled from hub.friendly-tech.com. Authenticate once per host before the first docker compose up.

docker login hub.friendly-tech.com

Enter the read-only pull credentials when prompted:

Field Value

Username

readonly

Password

fokxuw-fymte1-taSxyc

The readonly account provides pull-only access to the published images. It cannot push.

Alternatively, log in non-interactively:

echo "fokxuw-fymte1-taSxyc" | docker login hub.friendly-tech.com -u readonly --password-stdin

Verify authentication:

docker info | grep -A 5 Registry

5.1. Offline Servers

When the target host cannot reach hub.friendly-tech.com, pull the image on a machine that does have registry access, export it to an archive, transfer it, and load it on the offline host.

The connected machine can run Linux, macOS, or Windows — commands are given for both shells below. It does not need to be the same platform as the offline host.

An explicit --platform matching the offline host’s architecture is required. The images in Harbor are multi-arch (linux/amd64, linux/arm64); without --platform, docker pull selects the host architecture, which may not match the target. On an Apple Silicon (arm64) Mac or an arm64 Windows machine without --platform, the resulting archive will be arm64 and will fail with a platform does not match warning on amd64 servers. The examples below use linux/amd64; step 1 shows how to read the correct value off the offline host.

  1. On the offline host, find out which architecture it runs — this is the value you will pass as PLATFORM below. Ask Docker itself, since it reports what the daemon will actually accept:

    docker version --format '{{.Server.Arch}}'

    If Docker is not installed there yet, use the operating system instead — uname -m on Linux/macOS, or echo $env:PROCESSOR_ARCHITECTURE in PowerShell on Windows. Map the result:

    docker version reports uname -m / Windows reports Use as PLATFORM

    amd64

    x86_64 / AMD64

    linux/amd64

    arm64

    aarch64 / ARM64

    linux/arm64

    On Windows with Docker Desktop, {{.Server.Arch}} reports the architecture of the Linux VM that actually runs the containers — which is the value you want, not the Windows host’s own architecture.

  2. On a machine with registry access, log in. Run this on its own — it prompts for a password, so anything pasted after it on the same go would be swallowed as input:

    docker login hub.friendly-tech.com
  3. Pull the backend image and export it into an archive. Paste the whole block as-is; the only lines to change are PLATFORM and TAG on top.

    Linux / macOS (bash):

    PLATFORM=linux/amd64
    TAG=latest
    
    docker pull --platform "$PLATFORM" "hub.friendly-tech.com/api/provision-portal:$TAG"
    
    docker save "hub.friendly-tech.com/api/provision-portal:$TAG" \
      | gzip > "provision-api-$TAG.tar.gz"

    Windows (PowerShell):

    $PLATFORM = "linux/amd64"
    $TAG = "latest"
    
    docker pull --platform $PLATFORM "hub.friendly-tech.com/api/provision-portal:$TAG"
    
    docker save -o "provision-api-$TAG.tar" "hub.friendly-tech.com/api/provision-portal:$TAG"

    On Windows, always write the archive with docker save -o <file>. Piping or redirecting docker save from PowerShell (docker save …​ > file.tar) corrupts the archive, because the PowerShell pipeline re-encodes the stream as text instead of passing raw bytes. docker load then fails with unexpected EOF or invalid tar header. To compress for transfer, use the built-in tar.exe (Windows 10 1803+ / Server 2019+): tar.exe -czf provision-api.tar.gz provision-api-$TAG.tar.

  4. Transfer the archive to the offline host, together with the Provision Portal API folder (compose.yml, .env, xml/, config/) and the root .env — see Directory Structure for the full layout.

  5. On the offline host, load the archive and start the stack:

    Linux (bash):

    TAG=latest
    gzip -dc "provision-api-$TAG.tar.gz" | docker load
    cd /usr/local/ft-system
    docker compose up -d provision-api

    Windows (PowerShell):

    $TAG = "latest"
    docker load -i "provision-api-$TAG.tar"
    cd /usr/local/ft-system
    docker compose up -d provision-api

Confirm the image is present before starting, so a missing or mis-architected image fails here rather than mid-startup:

docker images hub.friendly-tech.com/api/provision-portal

Upgrades use the same flow: pull the new tags on the connected machine, transfer and load the archive, then docker compose up -d provision-api.

6. Preparation

Create the directories Provision Portal API needs on the host and point it at the infrastructure listed in Prerequisites.

6.1. Directory Structure

The platform layout gives each service one directory directly under /usr/local/ft-system, beside the host-level compose.yml and the shared .env. Create the backend directory and its three subdirectories — an empty log directory does not survive a download:

mkdir -p /usr/local/ft-system/provision-api/{xml,config,logs}
cd /usr/local/ft-system

6.1.1. Directory Layout

Once the files below are in place, the Provision Portal API part of /usr/local/ft-system/ looks like this:

/usr/local/ft-system/
├── compose.yml                             # from FT_DISK -- Provision Portal API (or the FT System <vendor> Stack)
├── .env                                    # shared stack environment
└── provision-api/
    ├── .env                                # per-service environment
    ├── xml/                                # -> /etc/app/xml     (XML_PATH)
    │   ├── Configuration.xml
    │   ├── objects.xml
    │   ├── params.xml
    │   ├── CSVSettings.xml
    │   ├── replaceCPE.xml
    │   └── CustomStatus.xml
    ├── config/                             # -> /etc/app/config  (CONFIG_PATH)
    │   └── encrypted-parameters.txt        # optional override
    └── logs/                               # -> /app/Log         (created above)
Path Content Backup

compose.yml

Stack definition, including the provision-api service block

Yes

.env

Shared stack environment — database connection, Flowable database, Northbound API URL, JWT, time zone, and the published ports compose.yml references as $\{…​}

Yes

provision-api/.env

Per-service environment — container ports, config paths, HikariCP pool sizing, TIMEOUT, TRANSACTION_DELAY and the log level

Yes

provision-api/xml/

Provisioning XML files (Configuration.xml, objects.xml, params.xml, CSVSettings.xml, replaceCPE.xml, CustomStatus.xml), mounted read-only at /etc/app/xml

Yes

provision-api/config/

encrypted-parameters.txt, mounted read-only at /etc/app/config

Yes

provision-api/logs/

Backend logs written to /app/Log inside the container — the provisioning trace files (CustomLog_*.txt, DebugLog_*.txt) and, thanks to LOG_PATH, the Spring Boot application.log. Grows unbounded without logrotate; see Logs

No

The web interface is deployed from its own FT_DISK folder into the sibling directory /usr/local/ft-system/provision-portal/ — see Provision Portal UI — Directory Structure.

This guide follows the platform layout defined in Full System Deployment: the directory is provision-api/, the XML and configuration files live in its xml/ and config/ subdirectories, and both are mounted read-only under /etc/app. The docker/compose.yml shipped in this repository is a developer stack: it runs both services from one file and mounts different paths. If your compose.yml came from that repository rather than from FT_DISK, check its volumes: and env_file: entries and adjust the paths accordingly.

XML_PATH and CONFIG_PATH must stay pointed at /etc/app/xml/ and /etc/app/config/, the two mount targets. compose.yml sets both in the environment: block of the provision-api service; their application defaults are classpath:/xml/ and classpath:config/ — the copies inside the image — so clearing either variable makes the container ignore the mounted directory silently, and every edit made on the host has no effect.

6.1.2. FT_DISK Downloads

Download the following from FT_DISK — Provision Portal API; the root .env comes from the FT System MySQL Stack / FT System Oracle Stack folder or from the sample in Environment Configuration:

Component FT_DISK Target Files

Stack definition

FT_DISK — Provision Portal API

/usr/local/ft-system/compose.yml

compose.yml — single-service, in the platform layout (Docker Compose); compose.debug.yml is the optional JDWP override. Skip both on a host that runs the full FT System stack: its compose.yml already contains this service block

Shared environment

FT System MySQL Stack / FT System Oracle Stack

/usr/local/ft-system/.env

.env — shared by every FT service on the host

Provision Portal API environment

FT_DISK — Provision Portal API

provision-api/.env

Only one file: the .env in the folder root

Provision Portal API configuration

FT_DISK — Provision Portal API

provision-api/xml/, provision-api/config/

All XML files → xml/, encrypted-parameters.txt → config/

The folder carries no per-vendor environment file, and that is deliberate: the database vendor is selected once, in the root .env (DB_PROFILE + SPRING_PROFILES_ACTIVE) — not by copying a .env.oracle over a .env — and compose.yml maps the generic DB_* values onto the vendor-specific names. The Oracle anchors are kept as comments in the shipped file. See [oracle-profile].

The XML files are also in the source repository at src/main/resources/xml/, and encrypted-parameters.txt at src/main/resources/config/encrypted-parameters.txt — both are baked into the jar as defaults, and a file at the mounted path overrides the copy inside the image.

For detailed descriptions of the XML files, see XML Configuration Guide.

6.2. Environment Configuration

The stack uses a two-layer environment file architecture — there is no environment file per database vendor.

File Purpose

.env

Database connection (DB_HOST, DB_PORT, DB_SERVICE, DB_USER, DB_PASSWORD, DB_TIMEZONE, DB_PROFILE), SPRING_PROFILES_ACTIVE, TZ, the Flowable database (FLOWABLE_DB_*), NORTHBOUND_API_URL, JWT_TOKEN_SECRET / JWT_TOKEN_EXPIRED, and the published port PROV_API_HTTP_PORT. Shared by every service on the host.

provision-api/.env

Per-service settings: container ports (PORT, HTTPS_PORT), config paths (XML_PATH, CONFIG_PATH), HikariCP tuning (DB_MAX_POOL_SIZE, DB_MIN_IDLE, DB_CONNECTION_TIMEOUT_MS), TIMEOUT, TRANSACTION_DELAY, FLOWABLE_CMMN_ASYNC_EXECUTOR_ACTIVATE, SPRING_THREADS_VIRTUAL_ENABLED and LOGGING_LEVEL_COM_FRIENDLY_PROVISIONPORTAL.

Both files are loaded by the env_file: list of the provision-api service, in that order. MySQL is the default; switching to Oracle changes variables inside the root .env, it does not swap a file — see [oracle-profile].

The MYSQL_*, ORACLE_* and FLOWABLE_* variables in the table below are not written by hand: compose.yml derives them from the generic DB_* and FLOWABLE_DB_* values through the x-db-env and x-flowable-env anchors (MYSQL_HOST: ${DB_HOST} and so on), so one database block in the root .env serves every service. JWT_SECRET and JWT_EXPIRATION are derived the same way, from JWT_TOKEN_SECRET and JWT_TOKEN_EXPIRED.

Every variable below is read by Provision Portal API itself. Default is the value that applies when the variable is unset — taken from application.yml / application-mysql.yml / application-oracle.yml where the placeholder carries one, otherwise from the environment files shipped on FT_DISK.

Variable Description Default Required

SPRING_PROFILES_ACTIVE

Active Spring profile; selects the datasource set and dialect (mysql, oracle)

mysql in the root .env; set to oracle for the Oracle profile

Yes

TZ

Container time zone, and therefore the JVM default zone

UTC in the image; Europe/Kyiv in the root .env

No

PORT

Plain-HTTP connector port inside the container (http.port); the published port maps to it

8082; 8080 in the shipped .env and pinned in compose.yml

Yes

HTTPS_PORT

Main connector port inside the container (server.port). server.ssl.enabled is true in application.yml, so it serves HTTPS from the keystore inside the image. Not published by compose.yml

8443

No

MYSQL_HOST

MySQL host for both the ACS and Flowable datasources; falls back to DB_HOST, then localhost

${DB_HOST} via the x-db-env anchor, then localhost

Yes on the mysql profile

MYSQL_PORT

MySQL port

${DB_PORT} via the x-db-env anchor; 3306 when unset

No

MYSQL_SCHEMA

Schema of the ACS datasource

ftacs (pinned by the x-db-env anchor)

No

MYSQL_USER

User of the ACS datasource

${DB_USER} via the x-db-env anchor; ftacs when unset

Yes on the mysql profile

MYSQL_PASSWORD

Password of the ACS datasource

${DB_PASSWORD} via the x-db-env anchor; ftacs when unset

Yes on the mysql profile

MYSQL_JDBC_URL

Full JDBC URL of the ACS datasource; overrides the URL built from the host, port and schema above

built from MYSQL_HOST, MYSQL_PORT, MYSQL_SCHEMA, DB_TIMEZONE

No

MYSQL_DRIVER_CLASS_NAME

JDBC driver class

com.mysql.cj.jdbc.Driver

No

DB_TIMEZONE

serverTimezone written into the MySQL JDBC URLs

Europe/Kiev in application-mysql.yml; Europe/Kyiv in the root .env

No

ORACLE_HOST

Oracle listener host for both datasources; falls back to DB_HOST, then localhost

${DB_HOST} via the x-db-env anchor, then localhost

Yes on the oracle profile

ORACLE_PORT

Oracle listener port

${DB_PORT} via the x-db-env anchor; 1521 when unset

No

ORACLE_SERVICE

Oracle service name (thin driver)

${DB_SERVICE} via the x-db-env anchor; XEPDB1 when unset

No

ORACLE_USER

Account owning the ACS schema

${DB_USER} via the x-db-env anchor; ftacs when unset

Yes on the oracle profile

ORACLE_PASSWORD

Password for ORACLE_USER

${DB_PASSWORD} via the x-db-env anchor; ftacs when unset

Yes on the oracle profile

ORACLE_JDBC_URL

Full JDBC URL of the ACS datasource; overrides the URL built from the host, port and service above

built from ORACLE_HOST, ORACLE_PORT, ORACLE_SERVICE

No

ORACLE_DRIVER_CLASS_NAME

JDBC driver class

oracle.jdbc.OracleDriver

No

FLOWABLE_MYSQL_HOST

Flowable database host (MySQL); falls back to FLOWABLE_DB_HOST, then DB_HOST

${FLOWABLE_DB_HOST} via the x-flowable-env anchor

No

FLOWABLE_MYSQL_PORT

Flowable database port (MySQL); falls back to MYSQL_PORT

${FLOWABLE_DB_PORT} via the x-flowable-env anchor

No

FLOWABLE_MYSQL_SCHEMA

Flowable schema name (MySQL)

${FLOWABLE_DB_SCHEMA} via the x-flowable-env anchor; flowable when unset

No

FLOWABLE_MYSQL_USER

Flowable database user (MySQL)

${FLOWABLE_DB_USER} via the x-flowable-env anchor; flowable when unset

Yes on the mysql profile

FLOWABLE_MYSQL_PASSWORD

Flowable database password (MySQL)

${FLOWABLE_DB_PASSWORD} via the x-flowable-env anchor; flowable when unset

Yes on the mysql profile

FLOWABLE_ORACLE_HOST

Flowable database host (Oracle); falls back to FLOWABLE_DB_HOST, then DB_HOST

${FLOWABLE_DB_HOST} via the x-flowable-env anchor

No

FLOWABLE_ORACLE_PORT

Flowable listener port (Oracle); falls back to ORACLE_PORT

${FLOWABLE_DB_PORT} via the x-flowable-env anchor

No

FLOWABLE_ORACLE_SERVICE

Flowable service name (Oracle); falls back to ORACLE_SERVICE

${DB_SERVICE} via the x-flowable-env anchor

No

FLOWABLE_ORACLE_USER

Account owning the Flowable schema (Oracle)

${FLOWABLE_DB_USER} via the x-flowable-env anchor; flowable when unset

Yes on the oracle profile

FLOWABLE_ORACLE_PASSWORD

Password for FLOWABLE_ORACLE_USER

${FLOWABLE_DB_PASSWORD} via the x-flowable-env anchor; flowable when unset

Yes on the oracle profile

DB_MAX_POOL_SIZE

HikariCP maximum pool size for the ACS datasource; also the default for the Flowable pool

10

No

DB_MIN_IDLE

HikariCP minimum idle connections

5

No

DB_CONNECTION_TIMEOUT_MS

HikariCP connection timeout in milliseconds

30000

No

FLOWABLE_DB_MAX_POOL_SIZE, FLOWABLE_DB_MIN_IDLE, FLOWABLE_DB_CONNECTION_TIMEOUT_MS

Flowable pool overrides; each falls back to the DB_* value above

inherit DB_*

No

NORTHBOUND_API_URL

Base URL of the Northbound API instance

http://localhost:8080/iot-webservice; http://northbound-api:8080/iot-webservice in the root .env

Yes

XML_PATH

Spring resource path of the provisioning XML directory

classpath:/xml/; file:/etc/app/xml/ in the shipped .env and pinned in compose.yml

Yes

CONFIG_PATH

Spring resource path of the additional configuration directory (encrypted-parameters.txt)

classpath:config/; file:/etc/app/config/ in the shipped .env and pinned in compose.yml

Yes

TIMEOUT

Default timeout for device operations, in seconds

60

No

TRANSACTION_DELAY

Extra delay after a transaction response, in milliseconds; increase under heavy ACS load

1000

No

JWT_SECRET

HS512 signing key for the tokens issued to the UI REST API; the value in application.yml is a development default identical in every deployment

${JWT_TOKEN_SECRET} via compose.yml; hardcoded fallback in application.yml when unset

Yes in production

JWT_EXPIRATION

Token lifetime in milliseconds

${JWT_TOKEN_EXPIRED} via compose.yml; 86400000 (24 h) when unset

No

LOGGING_CUSTOM_BASE_PATH, LOGGING_CUSTOM_DIRECTORY

Location of the provisioning trace files (CustomLog_*.txt, DebugLog_*.txt)

user.dir and Log; pinned to /app and Log in compose.yml

No

LOG_PATH

Directory of the Spring Boot application.log (logback)

logs, relative to the working directory; pinned to /app/Log in compose.yml

No

LOGGING_LEVEL_COM_FRIENDLY_PROVISIONPORTAL

Log level for com.friendly.provisionportal; changeable at runtime without a restart — see Runtime Log Level

DEBUG in logback-spring.xml when unset; INFO in the shipped .env

No

FLOWABLE_CMMN_ASYNC_EXECUTOR_ACTIVATE

Flowable CMMN async job executor switch

false (also false in application.yml)

No

SPRING_THREADS_VIRTUAL_ENABLED

Virtual threads switch; application-oracle.yml already sets false, so the shipped value only changes behaviour on the mysql profile

true (application.yml), false on the oracle profile

No

The sample block below also shows DB_PORT, DB_SERVICE, DB_USER, DB_PASSWORD, DB_PROFILE, FLOWABLE_DB_*, JWT_TOKEN_SECRET, JWT_TOKEN_EXPIRED and PROV_API_HTTP_PORT. Provision Portal API reads only DB_HOST and DB_TIMEZONE directly (as the fallback host and the serverTimezone in application-mysql.yml / application-oracle.yml). The others reach the application through the x-db-env / x-flowable-env anchors and the environment: block in compose.yml, which map them to the MYSQL_* / ORACLE_* / FLOWABLE_* / JWT_* names the application expects. PROV_API_HTTP_PORT is consumed by Compose itself for the ports: mapping and never enters the container.

# /usr/local/ft-system/.env
TZ=Europe/Kyiv

# Database (existing) -- MySQL profile
DB_HOST=mysql                              # <-- the compose service name; an IP or hostname for an external database
DB_PORT=3306
DB_USER=ftacs
DB_PASSWORD=<your-db-password>             # <-- replace
DB_SERVICE=XEPDB1                          # Oracle only
DB_TIMEZONE=Europe/Kyiv
DB_PROFILE=mysql
SPRING_PROFILES_ACTIVE=mysql

# Flowable database (existing)
FLOWABLE_DB_HOST=${DB_HOST}
FLOWABLE_DB_PORT=${DB_PORT}
FLOWABLE_DB_SCHEMA=flowable
FLOWABLE_DB_USER=flowable
FLOWABLE_DB_PASSWORD=<flowable-password>   # <-- replace

# Northbound API (existing)
NORTHBOUND_API_URL=http://northbound-api:8080/iot-webservice

# JWT (existing, shared with the other APIs)
JWT_TOKEN_SECRET=<your-jwt-secret>         # <-- replace; openssl rand -base64 48
JWT_TOKEN_EXPIRED=36000000                 # milliseconds

# Published port (existing)
PROV_API_HTTP_PORT=8091

PROV_API_HTTP_PORT must stay in the root .env. Compose resolves $\{…​} in the compose file from two places only: the shell environment, and the .env in the project directory. Files named under env_file: are handed to the container at start-up; Compose never reads them while parsing the compose file. Moved into provision-api/.env, the port fails silently: the mapping falls back to the $\{PROV_API_HTTP_PORT:-8091} default and the container still starts, on the wrong port.

For Oracle, keep the same root .env and change the database values inside it — DB_PROFILE=oracle, SPRING_PROFILES_ACTIVE=oracle, DB_HOST=<your-oracle-host>, DB_PORT=1521 and DB_SERVICE=XEPDB1 (DB_USER / DB_PASSWORD and the FLOWABLE_DB_* block stay as they are). compose.yml maps them to the ORACLE_* and FLOWABLE_ORACLE_* names the application expects, so no per-vendor environment file is involved. provision-api/.env does not change: it already ships SPRING_THREADS_VIRTUAL_ENABLED=false, which Oracle needs. See Switching to Oracle for the full variable list, and Flowable Database Setup for the Oracle schema setup.

Create provision-api/.env with the per-service variables; the full platform-wide reference is Environment Variables Reference.

Click to expand a commented provision-api/.env reference
# =============================================================================
# Provision Portal API -- per-service environment
# =============================================================================
# Loaded after the root .env, so a name set here wins over the same name there.
# Database, Flowable, Northbound API, JWT and TZ settings are deliberately NOT
# in this file -- they come from the root .env and reach the application through
# the anchors in compose.yml, which override anything set here anyway.

# Container connector ports. The host port is set in the ports: mapping of
# compose.yml (8091 -> 8080), not here.
# PORT is the plain-HTTP connector the published port maps to.
PORT=8080

# HTTPS connector port (the main Spring connector, TLS from the keystore
# inside the image). Not published by default.
HTTPS_PORT=8443

# =============================================================================
# Configuration Paths (inside container)
# =============================================================================

# Provisioning XML files. compose.yml mounts ./provision-api/xml here.
XML_PATH=file:/etc/app/xml/

# Additional configuration files (encrypted-parameters.txt).
# compose.yml mounts ./provision-api/config here.
CONFIG_PATH=file:/etc/app/config/

# =============================================================================
# HikariCP Connection Pool
# Applied to the ACS datasource, and inherited by the Flowable datasource
# unless the FLOWABLE_DB_* variants are set.
# =============================================================================
DB_MAX_POOL_SIZE=10
DB_MIN_IDLE=5
DB_CONNECTION_TIMEOUT_MS=30000
# FLOWABLE_DB_MAX_POOL_SIZE=10
# FLOWABLE_DB_MIN_IDLE=5
# FLOWABLE_DB_CONNECTION_TIMEOUT_MS=30000

# =============================================================================
# Device Operations
# =============================================================================
# Max wait time (seconds) for device operations dispatched via Northbound API
TIMEOUT=60

# Extra delay (ms) after a transaction response; increase under heavy ACS load
TRANSACTION_DELAY=1000

# =============================================================================
# Flowable / Threads
# =============================================================================
# Disable the Flowable CMMN async executor (no CMMN jobs to poll here)
FLOWABLE_CMMN_ASYNC_EXECUTOR_ACTIVATE=false

# Virtual threads. Oracle JDBC pins them; application-oracle.yml already turns
# them off on that profile, so this line only matters on mysql.
SPRING_THREADS_VIRTUAL_ENABLED=false

# =============================================================================
# Logging
# =============================================================================
# Log level for the application package (com.friendly.provisionportal).
# Values: ERROR, WARN, INFO, DEBUG. Can also be changed at runtime via Actuator.
LOGGING_LEVEL_COM_FRIENDLY_PROVISIONPORTAL=INFO

PORT, XML_PATH, CONFIG_PATH, LOGGING_CUSTOM_BASE_PATH, LOGGING_CUSTOM_DIRECTORY, LOG_PATH, JWT_SECRET and JWT_EXPIRATION are also set in the environment: block of the provision-api service, and environment: always wins over env_file:. The values in provision-api/.env must agree with compose.yml; a change to any of them is made in compose.yml, not in the env file.

Replace JWT_TOKEN_SECRET before going to production. Left at its placeholder, application.yml falls back to a hardcoded development key that is identical in every deployment, and any holder of it can mint valid tokens for this instance. Generate one with openssl rand -base64 48 — HS512 needs at least 64 characters.

6.3. TLS Keystore (optional)

provision-api serves HTTPS on 8443 from a self-signed keystore inside the jar (classpath:keystore.p12, password friendly, key alias myapp, all set in application.yml). This is not overridden by anything in the shipped compose or environment files and is suitable for testing only. To use a custom certificate, extend the provision-api service in compose.yml: mount a PKCS12 keystore and add the matching Spring Boot overrides under its environment: block:

    volumes:
      - ./provision-api/config:/etc/app/config:ro
      - ./provision-api/xml:/etc/app/xml:ro
      - ./provision-api/logs:/app/Log
      - ./provision-api/my-keystore.p12:/etc/app/keystore.p12:ro   # add
    environment:
      SERVER_SSL_KEY_STORE: file:/etc/app/keystore.p12  # add
      SERVER_SSL_KEY_STORE_PASSWORD: <your-password>     # add
      SERVER_SSL_KEY_STORE_TYPE: PKCS12                  # add
      SERVER_SSL_KEY_ALIAS: <your-key-alias>             # add

server.ssl.key-alias is fixed to myapp in application.yml, so a replacement keystore created with any other alias (keytool -alias server …​, as the sibling FT guides use) is loaded but no matching key entry is found and the TLS connector fails to start. Either override SERVER_SSL_KEY_ALIAS with the alias your keystore actually uses, or generate the keystore with -alias myapp and drop that line.

Remember also to publish port 8443 (see the warning in Network Requirements) if this certificate needs to be reachable from outside the container.

7. Deployment

7.1. Startup Dependencies

provision-api has no depends_on in the shipped compose.yml — the ACS database, the Flowable schema and Northbound API are external to that stack, so Compose cannot gate on their health. All of them must be reachable before provision-api launches: Liquibase applies the Flowable schema migrations on first start, and the JDBC pools connect during context initialisation.

Wait for each of them explicitly before starting the service:

# Wait for the ACS database
until nc -z <db-host-ip> 3306; do sleep 2; done

# Wait for the Flowable database (often the same host)
until nc -z <flowable-db-host-ip> 3306; do sleep 2; done

# Wait for Northbound API (9880 is its published port; 8080 is the container port on the shared bridge)
until curl -sf http://<northbound-host-ip>:9880/iot-webservice/actuator/health; do sleep 2; done

# Then start Provision Portal API
docker compose up -d provision-api

The Flowable schema must exist. Liquibase creates and migrates the tables, but not the schema or the user — see Flowable Database Setup.

provision-api itself restarts cleanly at any time once the infrastructure is up — restart: unless-stopped reconnects it after a host reboot.

In the full FT System stack the same service block carries depends_on: northbound-api: condition: service_healthy. That single edge gates the whole chain — Northbound API waits for FTACS, FTACS for FT Configs Service, FT Configs Service for the database and Hazelcast — so docker compose up -d provision-api on a complete stack brings the dependencies up in the right order by itself. Only the Flowable schema still has to exist beforehand.

7.2. Docker Compose

Click to expand compose.yml (Provision Portal API)
x-db-env: &db-env
  DB_VENDOR: ${DB_PROFILE}
  MYSQL_HOST: ${DB_HOST}
  MYSQL_PORT: ${DB_PORT}
  MYSQL_USER: ${DB_USER}
  MYSQL_PASSWORD: ${DB_PASSWORD}
  MYSQL_SCHEMA: ftacs

x-flowable-env: &flowable-env
  FLOWABLE_MYSQL_HOST: ${FLOWABLE_DB_HOST}
  FLOWABLE_MYSQL_PORT: ${FLOWABLE_DB_PORT}
  FLOWABLE_MYSQL_SCHEMA: ${FLOWABLE_DB_SCHEMA}
  FLOWABLE_MYSQL_USER: ${FLOWABLE_DB_USER}
  FLOWABLE_MYSQL_PASSWORD: ${FLOWABLE_DB_PASSWORD}

services:

  provision-api:
    image: hub.friendly-tech.com/api/provision-portal:v4.0.1-b0.0.1
    container_name: provision-api
    # The database, the Flowable schema and Northbound API are external to this
    # stack, so there is no depends_on. They must be reachable before
    # provision-api starts -- DB_HOST, FLOWABLE_DB_HOST and NORTHBOUND_API_URL
    # in .env point at them.
    env_file:
      - .env
      - ./provision-api/.env
    environment:
      <<: [*db-env, *flowable-env]
      JWT_SECRET: ${JWT_TOKEN_SECRET}
      JWT_EXPIRATION: ${JWT_TOKEN_EXPIRED}
      PORT: "8080"  # app HTTP connector; port mapping and health check expect 8080
      # Without these two the mounted xml/ and config/ directories are ignored:
      # application-<vendor>.yml defaults them to classpath:/xml/ and
      # classpath:config/, so the application reads the copies shipped in the
      # image and every file you place on the host has no effect.
      XML_PATH: file:/etc/app/xml/
      CONFIG_PATH: file:/etc/app/config/
      # The provisioning trace files (CustomLog_*.txt, DebugLog_*.txt) are written
      # to logging.custom.base-path + logging.custom.directory.
      LOGGING_CUSTOM_BASE_PATH: /app
      LOGGING_CUSTOM_DIRECTORY: Log
      # Spring Boot application.log: without this logback writes to the
      # unmounted /app/logs.
      LOG_PATH: /app/Log
    ports:
      - "${PROV_API_HTTP_PORT:-8091}:8080"   # HTTP
      # - "8443:8443"                         # Optional: HTTPS connector, self-signed keystore inside the image
    volumes:
      - ./provision-api/config:/etc/app/config:ro  # encrypted-parameters.txt
      - ./provision-api/xml:/etc/app/xml:ro
      - ./provision-api/logs:/app/Log
    extra_hosts:
      - "host.docker.internal:host-gateway"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/prov-portal/soap/ProvWS?wsdl"]
      interval: 10s
      timeout: 5s
      start_period: 60s
      retries: 50
    restart: unless-stopped
    networks:
      - ft-network

networks:
  ft-network:
    driver: bridge

For Oracle, the env_file: list and the service block stay the same — only the two anchors change, and the DB_* values are updated in the root .env (see [oracle-profile]):

x-db-env: &db-env
  DB_VENDOR: ${DB_PROFILE}
  ORACLE_HOST: ${DB_HOST}
  ORACLE_PORT: ${DB_PORT}
  ORACLE_SERVICE: ${DB_SERVICE}
  ORACLE_USER: ${DB_USER}
  ORACLE_PASSWORD: ${DB_PASSWORD}

x-flowable-env: &flowable-env
  FLOWABLE_ORACLE_HOST: ${FLOWABLE_DB_HOST}
  FLOWABLE_ORACLE_PORT: ${FLOWABLE_DB_PORT}
  FLOWABLE_ORACLE_SERVICE: ${DB_SERVICE}
  FLOWABLE_ORACLE_USER: ${FLOWABLE_DB_USER}
  FLOWABLE_ORACLE_PASSWORD: ${FLOWABLE_DB_PASSWORD}

In the full FT System stack the same block additionally carries profiles: [api] and depends_on: northbound-api — COMPOSE_PROFILES in the root .env must then list api for a bare docker compose up -d to start it, and it has no extra_hosts entry (Docker Networking). Everything else, including the two-layer env_file: list and the mount paths, is identical.

7.2.1. Start the Stack

Start Provision Portal API only after the Flowable schema exists, and — when they are external to this stack — after the ACS database and Northbound API accept connections. See Startup Dependencies.

cd /usr/local/ft-system
docker login hub.friendly-tech.com
docker compose up -d provision-api
docker compose ps
# The backend takes up to a minute to start
docker compose logs -f provision-api | grep -i "started"
curl -s "http://localhost:8091/prov-portal/soap/ProvWS?wsdl" | head -1

To run the backend as a single container without Compose:

cd /usr/local/ft-system

docker run -d \
  --name provision-api \
  --restart unless-stopped \
  --env-file .env --env-file ./provision-api/.env \
  -e XML_PATH=file:/etc/app/xml/ \
  -e CONFIG_PATH=file:/etc/app/config/ \
  -e LOG_PATH=/app/Log \
  -e LOGGING_CUSTOM_BASE_PATH=/app \
  -e LOGGING_CUSTOM_DIRECTORY=Log \
  -e PORT=8080 \
  -p 8091:8080 \
  -v "$PWD/provision-api/xml:/etc/app/xml:ro" \
  -v "$PWD/provision-api/config:/etc/app/config:ro" \
  -v "$PWD/provision-api/logs:/app/Log" \
  --add-host=host.docker.internal:host-gateway \
  hub.friendly-tech.com/api/provision-portal:v4.0.1-b0.0.1

The -e flags above are the ones compose.yml sets in its environment: block; without them the container reads the XML and configuration copies baked into the image and writes its logs to the unmounted /app/logs. The database, Flowable and JWT values still come from the two --env-file layers — but docker run does not apply the x-db-env / x-flowable-env mapping, so add -e MYSQL_HOST=…​ -e MYSQL_USER=…​ -e MYSQL_PASSWORD=…​, -e FLOWABLE_MYSQL_USER=…​ -e FLOWABLE_MYSQL_PASSWORD=…​ (or the ORACLE_* / FLOWABLE_ORACLE_* equivalents) and -e JWT_SECRET=…​ as well. docker run --env-file also reads the file more literally than Compose does: it does not expand ${DB_HOST} in FLOWABLE_DB_HOST=${DB_HOST}, and an inline # comment after a value becomes part of that value — strip the comments from the copy you hand to docker run.

7.2.2. Remote Debugging (optional)

JDWP remote debugging is disabled by default, to avoid the CPU cost and security exposure of an externally reachable debug port. To enable it, copy the compose.debug.yml shipped in the same FT_DISK folder next to compose.yml and apply it as an override:

cd /usr/local/ft-system
docker compose -f compose.yml -f compose.debug.yml up -d provision-api
Click to expand compose.debug.yml
# Usage: docker compose -f compose.yml -f compose.debug.yml up -d provision-api
# Enables JDWP remote debugging on port 5005, bound to the loopback interface.
# JAVA_OPTS is read by docker-entrypoint.sh; nothing else in the stack sets it.

services:
  provision-api:
    ports:
      - "127.0.0.1:${PROV_API_DEBUG_PORT:-5005}:5005"
    environment:
      JAVA_OPTS: "-Doracle.jdbc.timezoneAsRegion=false -agentlib:jdwp=transport=dt_socket,server=y,address=*:5005,suspend=n"

JAVA_OPTS is read by docker-entrypoint.sh (java ${JAVA_OPTS} -jar /app/app.jar); the stack sets it nowhere else, so any heap flags you rely on have to be repeated in this override next to the agent. The debug port is bound to 127.0.0.1 only — it is not reachable from outside the host even with this override applied.

Never remove the 127.0.0.1: prefix from the port mapping to expose 5005 publicly. External scanners hitting an open JDWP port cause high CPU usage and are a remote-code-execution risk.

7.3. Connecting the UI

The web interface is the only other component that talks to this service, and it does so over exactly one link: Nginx on its side reverse-proxies browser calls to the URL in FT_PROV_API_URL. Browsers never reach provision-api directly. FT_PROV_API_URL lives in the root .env, and its value depends on where the web interface runs:

Web interface runs FT_PROV_API_URL

On this host

http://provision-api:8080/prov-portal/ — both containers are on the ft-network bridge, so the service name resolves and the container port is used

On another host

http://<this-host-ip>:8091/prov-portal/ — the published port, which must match PROV_API_HTTP_PORT here. Use a literal IP or a name that already resolves: Nginx resolves the upstream while loading its configuration, and an unresolvable name stops that container from starting

Nothing else is shared: the database, Flowable, Northbound API and JWT settings are read by this service only — tokens are issued and validated by provision-api.

Across two hosts this hop is ordinary network traffic, and on port 8091 it is plain HTTP — provisioning payloads and the Authorization header travel unencrypted. Either keep both hosts on a trusted segment or VPN, or publish the HTTPS connector ("8443:8443" in the ports: list, see TLS Keystore (optional)) and point FT_PROV_API_URL at https://<this-host-ip>:8443/prov-portal/. Whether the web interface accepts an https:// upstream carrying a self-signed certificate has not been verified — test it before relying on it.

From the other host, the reachability check is the same one integrators use:

curl -s http://<this-host-ip>:8091/prov-portal/actuator/health

Everything on the web-interface side — the value Nginx actually received, the proxied path — is checked from there; see Provision Portal UI — Pointing at the Backend.

8. Verification

8.1. Startup Log

docker compose ps
# Expected: provision-api "Up" / "healthy"

docker compose logs provision-api --tail 50
# Look for: "Started ProvisionPortalApplication in XX.XXX seconds"

The healthcheck has start_period: 60s, so provision-api legitimately reports starting for up to a minute before turning healthy; requests proxied through the web interface return 502 during that minute.

8.2. Endpoint Checks

# Provision API health (plain HTTP, published port)
curl -s http://localhost:8091/prov-portal/actuator/health
# Expected: {"status":"UP"}

# SOAP WSDL
curl -s http://localhost:8091/prov-portal/soap/ProvWS?wsdl | head -5
# Expected: beginning of the WSDL XML document

# Swagger UI (open in browser)
# http://localhost:8091/prov-portal/swagger-ui/index.html

The Docker healthcheck for provision-api in the stack compose.yml curls http://localhost:8080/prov-portal/soap/ProvWS?wsdl inside the container — the plain-HTTP connector, the same one the published port maps to. The HTTPS connector on 8443 is always present as well but is not published unless you add the mapping yourself; see Network Requirements.

8.3. Database Connectivity

/prov-portal/actuator/health reports both datasources (management.endpoint.health.show-details: always), so a database problem shows up there before it shows up in a client:

curl -s http://localhost:8091/prov-portal/actuator/health | grep -i -A5 db

Reachability from inside the backend container, and the values actually applied:

docker compose exec provision-api nc -zv <db-host> 3306        # Oracle: 1521
docker compose exec provision-api env | grep -E 'MYSQL|ORACLE|FLOWABLE'

On first start, the Flowable tables are created by Liquibase; confirm they exist in the Flowable schema afterwards (ACT_RU_EXECUTION, ACT_RE_PROCDEF, and the rest of the ACT_* set).

9. Port Reference

Ports are written as published → container. Across hosts you connect to the published port; inside the Docker bridge you connect to the container port.

9.1. HTTP / HTTPS

Port Protocol Purpose Exposure

8091 → 8080

HTTP

provision-api REST/SOAP API, Swagger UI, Actuator (PROV_API_HTTP_PORT)

Public

8443

HTTPS

provision-api same endpoints over TLS, self-signed keystore inside the image (HTTPS_PORT in provision-api/.env)

Internal (bridge only) — not published by the shipped compose.yml

127.0.0.1:5005 → 5005

TCP (JDWP)

provision-api remote debugging, only with a compose.debug.yml override (PROV_API_DEBUG_PORT)

Localhost

All three published ports are host-level values and live in /usr/local/ft-system/.env, not in provision-api/.env. Compose does not interpolate $\{…​} from an env_file: entry — see [env-file-is-not-interpolated]. The full platform port map is in Full System Deployment — Port Reference.

9.2. Outbound Connections

Destination Port Protocol Purpose

ACS database

3306 (Oracle: 1521)

TCP

JDBC connection to the ftacs schema

Flowable database

3306 (Oracle: 1521)

TCP

JDBC connection to the Flowable schema; Liquibase migrations on first start

Northbound API

8080 on the shared bridge; 9880 published across hosts

HTTP

REST calls that dispatch TR-069 provisioning operations

hub.friendly-tech.com

443

HTTPS

Image pulls (docker login, docker compose pull) — not needed on offline hosts

9.3. HTTP Endpoints

All backend paths are under the servlet context path /prov-portal (application.yml).

Method Path Purpose Auth

GET

/prov-portal/actuator/health

Liveness and datasource details

None

GET

/prov-portal/actuator/prometheus

Prometheus scrape endpoint (micrometer registry)

None

POST

/prov-portal/ui/auth/login

UI login, issues the JWT

None

GET

/prov-portal/ui/server/version

Application version

None

GET, POST

/prov-portal/ui/**

UI REST API (devices, provisioning, users)

JWT bearer token

POST

/prov-portal/soap/ProvWS

SOAP provisioning endpoint (CXF servlet mapped at /soap/*)

None

GET

/prov-portal/soap/ProvWS?wsdl

WSDL for the SOAP endpoint

None

GET

/prov-portal/swagger-ui/index.html

Swagger UI

None

GET

/prov-portal/api-docs

OpenAPI documents (UI, REST, SOAP)

None

The SOAP and integration REST endpoints are permitAll in SecurityConfig, and the catch-all rule is anyRequest().permitAll(). Only the /ui/** paths require a token. Do not expose 8091 outside a trusted network without a reverse proxy that enforces access control.

10. Stack Management

10.1. Logs

docker compose logs -f provision-api

Log files are also written to the host through the provision-api/logs/ bind mount (→ /app/Log).

The backend writes two independent streams, each located by its own variable:

Files Variables Notes

CustomLog_<dd.MM.yyyy>.txt, DebugLog_<dd.MM.yyyy>.txt

LOGGING_CUSTOM_BASE_PATH + LOGGING_CUSTOM_DIRECTORY

Provisioning trace written by the application’s own logger. Created lazily, on the first provisioning operation — an empty directory right after deployment is expected, not a fault.

application.log, rotated daily to application.<yyyy-MM-dd>.log.gz (30 days kept)

LOG_PATH

The Spring Boot log. Without LOG_PATH logback falls back to a path relative to the working directory (/app/logs), which is not bind-mounted, so the file never reaches the host.

The backend process runs as appuser (UID 1001) — the entrypoint drops privileges even though the container starts as root. If the host directory is not writable for that UID, the application reports the failure as a warning on stdout and writes no file. Check docker compose logs provision-api and, if needed, run chown -R 1001:1001 logs on the host.

10.1.1. Runtime Log Level

The log level for com.friendly.provisionportal can be changed at runtime, without a restart, through Spring Boot Actuator (management.endpoints.web.exposure.include: '*' is on by default).

# View the current level
curl http://localhost:8091/prov-portal/actuator/loggers/com.friendly.provisionportal

# Switch to INFO
curl -X POST http://localhost:8091/prov-portal/actuator/loggers/com.friendly.provisionportal \
  -H 'Content-Type: application/json' \
  -d '{"configuredLevel": "INFO"}'

# Switch to DEBUG -- business errors then include stack traces
curl -X POST http://localhost:8091/prov-portal/actuator/loggers/com.friendly.provisionportal \
  -H 'Content-Type: application/json' \
  -d '{"configuredLevel": "DEBUG"}'
All POST requests return HTTP 204 No Content with an empty body — this is the expected success response. Use the GET request above to verify the change.

This resets on container restart. To change the level persistently, set LOGGING_LEVEL_COM_FRIENDLY_PROVISIONPORTAL in provision-api/.env (values: ERROR, WARN, INFO, DEBUG).

10.2. Start, Stop, Restart

Command Description

docker compose up -d provision-api

Start the backend in the background

docker compose stop provision-api

Stop the backend without touching the rest of the stack

docker compose down

Stop and remove every container of the ft-system project

docker compose ps

Show container status

docker compose restart provision-api

Restart the API only

docker compose pull

Update the image to its latest version

docker stats

View resource usage

10.3. Shell Access

docker compose exec provision-api sh                          # enter the API container
docker compose exec provision-api ls -la /etc/app             # check the mounted config directory
docker compose exec provision-api env | grep MYSQL            # check environment variables actually applied
The backend image drops privileges to the unprivileged appuser (uid 1001) via gosu in docker-entrypoint.sh, so an interactive shell has no write access outside /app and the mounts.

10.4. Updating Provision Portal

# 1. Back up configuration
cd /usr/local/ft-system
tar -czf provision-api-backup-$(date +%Y%m%d).tar.gz   .env compose.yml provision-api/.env provision-api/xml provision-api/config

# 2. Pull the latest image
docker compose pull provision-api

# 3. Recreate the container with the new image
docker compose up -d provision-api

# 4. Verify
curl -s http://localhost:8091/prov-portal/actuator/health

Liquibase migrates the Flowable schema automatically on the first start of a new version.

Back up the ACS and Flowable databases before pulling a new image — Liquibase migrations apply on first start and are not reversible from within Provision Portal.

11. Production Checklist

  • JWT secret — ships as a placeholder. Replace JWT_TOKEN_SECRET with 64+ random characters and review JWT_TOKEN_EXPIRED in the root .env; see the warning in Environment Configuration. Left unreplaced, every deployment shares the same hardcoded signing key.

  • Database credentials — replace the DB_PASSWORD / FLOWABLE_DB_PASSWORD placeholders in the root .env, and review DB_USER / FLOWABLE_DB_USER, which ship as ftacs / flowable.

  • Northbound API URL — NORTHBOUND_API_URL points at the correct instance: the shipped http://northbound-api:8080/iot-webservice resolves only when Northbound API runs in the same stack on this host; across hosts use its address and published port (9880).

  • Database hosts — DB_HOST and FLOWABLE_DB_HOST in the root .env point at the real database, not at the mysql / oracle compose service when the database is external.

  • TLS certificate — replace the self-signed keystore.p12 inside the image if HTTPS on the API is exposed; see TLS Keystore (optional). If not, keep 8443 unpublished.

  • API exposure — SecurityConfig leaves the SOAP and integration REST endpoints unauthenticated (anyRequest().permitAll()); put a reverse proxy in front of 8091 or keep it off public networks.

  • CORS — SecurityConfig combines allowedOriginPattern("*") with allowCredentials(true), effectively reflected-origin CORS with credentials. Review before exposing this service beyond a trusted network; this is an application-code change, not something .env controls.

  • File permissions — restrict both env layers: chmod 600 .env provision-api/.env.

  • Log management — configure Docker log rotation: --log-opt max-size=50m --log-opt max-file=5.

  • Timezone — set TZ in the root .env (ships as Europe/Kyiv); it is also the JVM default zone.

  • Resource limits — nothing in the stack sets JAVA_OPTS for this service, so the JVM sizes its heap from the memory it sees; without a container limit that is the host’s total RAM. Add deploy.resources.limits.memory to the service in compose.yml, and JAVA_OPTS: -XX:MaxRAMPercentage=70 under environment: if the default 25 % is too small.

  • Image tag — pin image: to a release tag; the shipped compose.yml uses latest, so a docker compose pull can move the deployment to a new version unintentionally.

  • Debug port — no compose.debug.yml override is applied in production; port 5005 is not published.

12. Troubleshooting

12.1. Container Fails to Start

Symptom: provision-api exits immediately after docker compose up.

Fix:

  1. Read the error from the logs:

    docker compose logs provision-api
    docker events --filter container=provision-api
  2. Confirm both environment layers are loaded:

    docker compose exec provision-api env | grep -E 'MYSQL|ORACLE|TIMEOUT|HTTPS_PORT'

    Values from the root .env and from provision-api/.env must both be present — an env_file: entry pointing at a missing file makes Compose fail outright, but a file in the wrong directory silently leaves the defaults in place.

  3. Confirm provision-api/config/ and provision-api/xml/ exist and are populated — an empty mount at /etc/app/xml is a common cause.

12.2. Could not resolve placeholder 'XML_PATH'

Symptom: PlaceholderResolutionException: Could not resolve placeholder 'XML_PATH'

Fix:

  1. Confirm the environment: block of the provision-api service still sets XML_PATH: file:/etc/app/xml/ — that is where it comes from, not from either .env layer.

  2. Validate inside the container: docker compose exec provision-api env | grep XML_PATH.

12.3. XML Configuration Files Not Found

Symptom: Application logs show a FileNotFoundException for the provisioning XML files.

Fix:

  1. Confirm the XML files exist under provision-api/xml/ on the host.

  2. Verify the volume mount: docker compose exec provision-api ls -la /etc/app/xml.

12.4. Database Connection Failure

Symptom: MySQL Communications link failure or Oracle ORA-12514 in the logs.

Fix:

  1. Test connectivity from inside the container:

    docker compose exec provision-api nc -zv <db-host> <db-port>
  2. Check DB_HOST, DB_PORT and the credentials in the root .env — the MYSQL_* / ORACLE_* names inside the container are derived from them by the compose anchors, so fix the DB_* values, not the derived ones.

12.5. Flowable Database Not Found

Symptom: Unknown database 'flowable' or ORA-01017: invalid username/password for the Flowable user in the logs.

Fix:

  1. Verify the Flowable schema exists (see Flowable Database Setup).

  2. Check FLOWABLE_DB_HOST and the Flowable credentials in the root .env.

12.6. Northbound API Connection Failure

Symptom: Connection refused or timeout errors when calling Northbound API.

Fix:

  1. Verify NORTHBOUND_API_URL in the root .env is correct and reachable: docker compose exec provision-api curl -sf $NORTHBOUND_API_URL/actuator/health.

  2. On the same stack, NORTHBOUND_API_URL should use the service name (http://northbound-api:8080/iot-webservice). When Northbound API runs elsewhere, use its host address and published port — never localhost, which inside the container points at the container itself.

12.7. Port Already in Use

Symptom: address already in use error on docker compose up.

Fix:

  1. Find the process using the port:

    lsof -i :8091
    # or
    docker ps
  2. Stop the conflicting process, or change PROV_API_HTTP_PORT in the root .env (see [env-file-is-not-interpolated] for why it lives there and not in provision-api/.env).

[[401-or-403-from-the-ui-api-calls]] === 401 or 403 on the /ui/** Endpoints

Symptom: UI login succeeds, subsequent calls return 401/403, or tokens issued before a restart stop working.

Cause: JWT_TOKEN_SECRET was set (or changed) in the root .env and the container recreated — all previously issued tokens are invalidated because they were signed with the old key.

Fix: re-authenticate. If this happens on every restart even without changing the secret, confirm JWT_TOKEN_SECRET is actually set in the root .env and arrives in the container as JWT_SECRET (docker compose exec provision-api env | grep JWT) — otherwise the application falls back to its hardcoded default; see Production Checklist.

12.8. Permission Denied on Mounted Volumes

Symptom: nothing appears in provision-api/logs/, and docker compose logs provision-api reports that a log file could not be opened.

Cause: the entrypoint drops privileges to appuser (uid 1001) with gosu, so the container writes to /app/Log as that uid. A host directory the uid cannot write to leaves the application logging a warning to stdout and producing no file.

Fix:

sudo chown -R 1001:1001 /usr/local/ft-system/provision-api/logs
chmod -R 755 /usr/local/ft-system/provision-api/logs

The xml/ and config/ mounts are read-only, so they need read access only.

12.9. Out of Memory

Symptom: java.lang.OutOfMemoryError in the logs, or the container is killed by the OOM killer.

Fix:

  1. Set a container memory limit — add deploy.resources.limits.memory to the provision-api service in compose.yml; the JVM sizes its heap from that limit (25 % by default).

  2. To use more of it, add JAVA_OPTS: -XX:MaxRAMPercentage=70 under the environment: block — docker-entrypoint.sh passes JAVA_OPTS to java.

  3. If the problem persists, capture a heap dump and analyse it for leaks.

12.10. Getting Support

When raising an issue, attach the output of the following so the deployment state is unambiguous:

docker compose ps
docker compose logs --tail 200 provision-api
docker compose exec provision-api env | grep -E 'SPRING_PROFILES_ACTIVE|DB_|MYSQL_HOST|ORACLE_HOST|FLOWABLE|NORTHBOUND|XML_PATH|CONFIG_PATH|HTTPS_PORT'
curl -s http://localhost:8091/prov-portal/actuator/health

Redact passwords before sharing. Include the image tags in use (docker compose images) and the contents of version.xml if you built the image yourself.