Installation & Deployment

1. Overview

This guide deploys Provision Portal — the two containers provision-api (Spring Boot 3.5 / Java 25 backend) and provision-portal (Angular UI served by Nginx), both defined in docker/compose.yml under the Compose project name ft-provision-portal. Docker Compose is the only supported deployment method for this repository: the Gradle build produces a fat jar (bootJarprovision-portal.jar) that is baked into the image, and no distribution archive is built.

The infrastructure Provision Portal 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 Provision Portal at it. To deploy that infrastructure, see All in one server deployment or Separate server deployment — Server E.

Clients / operators  ->  Provision Portal UI (Nginx)  ->  Provision API (Spring Boot)  ->  MySQL / Oracle (ACS + Flowable schemas)  +  Northbound API
Container Image Role

provision-api

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

REST/SOAP backend, Flowable workflow engine, provisioning logic

provision-portal

hub.friendly-tech.com/ui/provision-portal

Angular UI served by Nginx; proxies to provision-api

The two images live under different Harbor namespaces despite sharing the same base name: the backend is api/provision-portal, the UI is ui/provision-portal. Confusing the two pulls the wrong container.

provision-portal (the UI) has depends_on: provision-api: condition: service_healthy — it will not start until the API container reports healthy.

2. Prerequisites

2.1. Host Requirements

Component Minimum Recommended Notes

Docker Engine

20.10

Latest stable

compose.yml uses the Compose v2 name: key and depends_on.condition: service_healthy

Docker Compose

2.0

Latest stable

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

RAM

2 GB

4 GB

FT_PROV_API_JAVA_RAM sizes the JVM heap as a percentage of available memory (-XX:MaxRAMPercentage=70)

Free disk space

1 GB

10 GB

Images, plus provision-api/logs and provision-portal/nginx/logs bind mounts

2.2. Required External Dependencies

These services must be installed, running, and reachable from this host before the Provision Portal stack 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.0; Oracle version: verify with product owner

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.0; Oracle version: verify with product owner

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 UI and the REST/SOAP APIs accept requests but no device is ever provisioned

8080 (HTTP) by the shipped NORTHBOUND_API_URL default

Yes

The MySQL minimum comes from application.yml (mysql.dialect: org.hibernate.dialect.MySQL8Dialect); the bundled drivers are com.mysql:mysql-connector-j:9.2.0 and com.oracle.database.jdbc:ojdbc11:23.6.0.24.10 (build.gradle). 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 — ProvAPI for the backend’s deployment files (compose.yml, the .env templates, the XML configuration package), and to FT_DISK — provision-portal for the UI’s TLS certificate.

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. Setting SPRING_THREADS_VIRTUAL_ENABLED=false in the environment file (see Environment Configuration) makes that explicit for anyone reading the deployment.

3. Network Requirements

Connections the two containers open outwards, and the published ports operators 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 (shipped default)

TCP

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

provision-api (from the UI container)

8080

TCP

Nginx proxies UI calls to http://provision-api:8080/prov-portal/ over the Compose network — not published to the host

provision-portal UI (from clients / operators)

8890 (HTTP) / 8893 (HTTPS)

TCP

Web interface

provision-api (from clients, integrators 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 bundled self-signed keystore), and the Docker healthcheck curls it internally — but the shipped docker/compose.yml does not publish 8443 to the host, only 8080. 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>

3.1. Docker Networking

Neither service declares a custom networks: entry, so Compose puts both containers on its default project network and they reach each other by service name (provision-api, provision-portal).

provision-api declares extra_hosts: - "host.docker.internal:host-gateway". 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.

4. Registry Authentication

Both images are 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

4.1. Offline Servers

When the target host cannot reach hub.friendly-tech.com, pull both images on a machine that does have registry access, export them 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 both images and export them into a single 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 pull --platform "$PLATFORM" "hub.friendly-tech.com/ui/provision-portal:$TAG"
    
    docker save \
      "hub.friendly-tech.com/api/provision-portal:$TAG" \
      "hub.friendly-tech.com/ui/provision-portal:$TAG" \
      | gzip > "provision-portal-$TAG.tar.gz"

    Windows (PowerShell):

    $PLATFORM = "linux/amd64"
    $TAG = "latest"
    
    docker pull --platform $PLATFORM "hub.friendly-tech.com/api/provision-portal:$TAG"
    docker pull --platform $PLATFORM "hub.friendly-tech.com/ui/provision-portal:$TAG"
    
    docker save -o "provision-portal-$TAG.tar" `
      "hub.friendly-tech.com/api/provision-portal:$TAG" `
      "hub.friendly-tech.com/ui/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 bundled tar.exe (Windows 10 1803+ / Server 2019+): tar.exe -czf provision-portal.tar.gz provision-portal-$TAG.tar.

  4. Transfer the archive to the offline host, together with compose.yml, the root .env and the per-service provision-api/.env and provision-portal/.env, the TLS files in provision-portal/ssl/ (friendly.crt, friendly.key) and the XML files in provision-api-conf/xml/ — 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-portal-$TAG.tar.gz" | docker load
    docker compose up -d

    Windows (PowerShell):

    $TAG = "latest"
    docker load -i "provision-portal-$TAG.tar"
    docker compose up -d

Confirm both images are 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
docker images hub.friendly-tech.com/ui/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.

This covers the Provision Portal API and UI images only. The MySQL or Oracle database and the Northbound API are separate images on their own hosts — for the offline procedure covering a whole stack, see All in one server deployment — Offline Servers, which derives the image list from compose.yml itself.

5. Preparation

5.1. Directory Structure

compose.yml mounts persistent data under ${DATA_FOLDER}; the shipped .env template defaults DATA_FOLDER to /usr/local. Set it to /usr/local/ft-system so this stack sits under the same root as every other Friendly Tech service instead of scattering directories directly under /usr/local/. Create the directories, and keep compose.yml and .env at that root:

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

5.1.1. Directory Layout

The expected layout on the host with DATA_FOLDER=/usr/local/ft-system, with the container path each directory is mounted to:

/usr/local/ft-system/
├── compose.yml                             # from FT_DISK
├── compose.debug.yml                       # optional: JDWP remote debugging override
├── .env                                    # shared stack environment
├── provision-api/
│   ├── .env                                # per-service environment
│   └── logs/                               # -> /app/Log                (created automatically)
├── provision-api-conf/                     # -> /etc/app  (whole directory, one mount)
│   ├── config/                             # CONFIG_PATH=file:/etc/app/config/
│   │   └── encrypted-parameters.txt        # optional override
│   └── xml/                                # XML_PATH=file:/etc/app/xml/
│       ├── Configuration.xml
│       ├── objects.xml
│       ├── params.xml
│       ├── CSVSettings.xml
│       ├── replaceCPE.xml
│       └── CustomStatus.xml
└── provision-portal/
    ├── .env                                # per-service environment
    ├── ssl/                                # -> /etc/nginx/ssl:ro       (Nginx TLS)
    │   ├── friendly.crt
    │   └── friendly.key
    └── nginx/logs/                         # -> /var/log/nginx
Path Content Backup

compose.yml

Stack definition for provision-api and provision-portal, from FT_DISK

Yes

compose.debug.yml

Optional override that enables JDWP on provision-api; see Docker Compose

Yes

.env

Shared stack environment — database connection, Flowable database, inter-service URLs, JWT, TZ, DATA_FOLDER

Yes

provision-api/.env

Per-service environment for the backend — published and in-container ports, JVM options, config paths, timeouts, log level

Yes

provision-api/logs/

Backend application logs, written to /app/Log inside the container

No

provision-api-conf/config/

encrypted-parameters.txt; optional override of the copy bundled in the jar

Yes

provision-api-conf/xml/

Provisioning XML files (Configuration.xml, objects.xml, params.xml, CSVSettings.xml, replaceCPE.xml, CustomStatus.xml)

Yes

provision-portal/.env

Per-service environment for the UI — published HTTP and HTTPS host ports

Yes

provision-portal/ssl/

TLS certificate and key for the UI’s Nginx (friendly.crt, friendly.key)

Yes

provision-portal/nginx/logs/

Nginx access and error logs

No

compose.yml mounts the backend configuration as a single directory — ${DATA_FOLDER}/provision-api-conf:/etc/app — and XML_PATH / CONFIG_PATH point at the xml/ and config/ subdirectories inside it. It is also the SPRING_CONFIG_ADDITIONAL_LOCATION (file:/etc/app/), so an application.yml dropped into provision-api-conf/ is picked up as an additional Spring configuration source.

DATA_FOLDER is read from the root .env (default /usr/local in the shipped template). Set it to /usr/local/ft-system as shown, or to any dedicated path — the compose mounts resolve ${DATA_FOLDER}/provision-portal, ${DATA_FOLDER}/provision-api-conf and ${DATA_FOLDER}/provision-api against it.

Download the following from FT_DISK:

Component FT_DISK Target directory Files

Provision API — stack definition

FT_DISK — ProvAPI

/usr/local/ft-system/

compose.yml, the environment file templates (.env, provision-api/.env, provision-portal/.env), optionally compose.debug.yml

Provision API — provisioning XML

FT_DISK — ProvAPI

provision-api-conf/xml/

All XML files (Configuration.xml, objects.xml, params.xml, CSVSettings.xml, replaceCPE.xml, CustomStatus.xml)

Provision Portal UI

FT_DISK — provision-portal

provision-portal/ssl/

friendly.crt, friendly.key

The XML files are also available 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; placing a file at the mounted path overrides the bundled one. compose.yml and the environment file templates are in docker/ in the source repository.

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

5.2. Environment Configuration

The stack uses a two-layer environment file architecture. There is no separate file per database vendor — both containers read the shared root .env first, then their own per-service .env:

File Purpose

.env

Database connection, Flowable database, inter-service URLs, JWT, TZ, DATA_FOLDER — shared by both containers

<service>/.env

Per-service host ports, in-container ports, JVM settings, timeouts and log level

Both files are listed in env_file: in compose.yml, root first so the per-service file wins on a conflict:

    env_file:
      - .env
      - ./provision-api/.env

The three files come from FT_DISK together with compose.yml (they are also in docker/ in the source repository). Put the shared one at the stack root and the per-service ones next to each service’s directory, then edit the values:

cd /usr/local/ft-system
# from the FT_DISK ProvAPI bundle you downloaded above
cp <bundle>/.env                    ./.env
cp <bundle>/provision-api.env       ./provision-api/.env
cp <bundle>/provision-portal.env    ./provision-portal/.env

If the bundle you received contains only compose.yml and the root .env, write the two per-service files yourself — every variable below marked Required must be present, or the container exits at startup with Could not resolve placeholder …​ (see Troubleshooting):

cd /usr/local/ft-system

cat > provision-api/.env <<'EOF'
PORT=8080
HTTPS_PORT=8443
FT_PROV_API_HTTP_PORT=8091
FT_PROV_API_JAVA_RAM=-XX:InitialRAMPercentage=50 -XX:MaxRAMPercentage=70
XML_PATH=file:/etc/app/xml/
CONFIG_PATH=file:/etc/app/config/
TIMEOUT=60
TRANSACTION_DELAY=1000
LOGGING_LEVEL_COM_FRIENDLY_PROVISIONPORTAL=INFO
EOF

cat > provision-portal/.env <<'EOF'
FT_PROV_PORTAL_HTTP_PORT=8890
FT_PROV_PORTAL_HTTPS_PORT=8893
EOF

Then fill in the root .env — database hosts, credentials, NORTHBOUND_API_URL, DATA_FOLDER=/usr/local/ft-system, TZ and JWT_SECRET — using the table below.

MySQL is the default; on MySQL no vendor-specific change is needed beyond replacing the passwords. Selecting Oracle is not a file swap — it is a set of variables changed inside the same root .env (SPRING_PROFILES_ACTIVE, the database host, port, service name and credentials). See Switching to Oracle for the full variable list, and Flowable Database Setup for the Oracle schema setup.

Table 1. Root .env — shared by both containers
Variable Description Default Required

SPRING_PROFILES_ACTIVE

Database profile: mysql or oracle

mysql

Yes

DATA_FOLDER

Base directory on the host for all bind mounts

/usr/local

Yes

TZ

Container timezone, also passed to the JVM as -Duser.timezone

Europe/Kyiv

Yes

DB_TIMEZONE

Timezone used in the MySQL JDBC URL (serverTimezone)

Europe/Kyiv

Yes (MySQL)

MYSQL_HOST / ORACLE_HOST

ACS database host

<your-mysql-host> / <your-oracle-host> placeholder

Yes

MYSQL_PORT / ORACLE_PORT

ACS database port

3306 / 1521

Yes

MYSQL_SCHEMA / ORACLE_SERVICE

ACS schema name (MySQL) or Oracle service name

ftacs / XEPDB1

Yes

MYSQL_USER / ORACLE_USER

ACS database user

ftacs

Yes

MYSQL_PASSWORD / ORACLE_PASSWORD

ACS database password — replace the template value

ftacs

Yes

FLOWABLE_MYSQL_HOST / FLOWABLE_ORACLE_HOST

Flowable database host (may be the same instance as the ACS database)

<your-mysql-host> / <your-oracle-host> placeholder

Yes

FLOWABLE_MYSQL_PORT / FLOWABLE_ORACLE_PORT

Flowable database port

3306 / 1521

Yes

FLOWABLE_MYSQL_SCHEMA / FLOWABLE_ORACLE_SERVICE

Flowable schema name (MySQL) or Oracle service name

flowable / XEPDB1

Yes

FLOWABLE_MYSQL_USER / FLOWABLE_ORACLE_USER

Flowable database user

flowable

Yes

FLOWABLE_MYSQL_PASSWORD / FLOWABLE_ORACLE_PASSWORD

Flowable database password — replace the template value

flowable

Yes

NORTHBOUND_API_URL

Base URL of the Northbound API instance

http://<northbound-host>:8080/iot-webservice placeholder

Yes

JWT_SECRET

Signing key for the UI REST API tokens. Not present in the shipped template — see the warning below

hardcoded fallback in application.yml

No (optional) — set it in production

JWT_EXPIRATION

Token lifetime in milliseconds

86400000 (24 h)

No (optional)

compose.yml in this repository passes the vendor-specific names (MYSQL_*, ORACLE_*) straight through to both applications. In the platform stack the same values are written once as generic DB_* variables and mapped to the vendor-specific names by the x-db-env / x-flowable-env YAML anchors in compose.yml — see Environment Configuration in the all-in-one-server guide.

Table 2. provision-api/.env — backend
Variable Description Default Required

PORT

Backend HTTP port inside the container (the additional Tomcat connector). Keep 8080 — the compose port mapping expects the application here

8080

Yes

HTTPS_PORT

Backend HTTPS port inside the container (the main connector, TLS via the bundled classpath keystore). Not published by default

8443

Yes

FT_PROV_API_HTTP_PORT

Host port published for the backend (→ 8080). Set it explicitly — this repository’s docker/compose.yml still falls back to 8085, which is the platform’s Service API port; see the warning below

8091

Yes

FT_PROV_API_DEBUG_PORT

Host port for JDWP remote debugging; only read when compose.debug.yml is applied

5005 (commented out in the template)

No (optional)

FT_PROV_API_JAVA_RAM

JVM heap sizing flags for the backend container

-XX:InitialRAMPercentage=50 -XX:MaxRAMPercentage=70

Yes

XML_PATH

Location of the provisioning XML files inside the container

file:/etc/app/xml/

Yes

CONFIG_PATH

Location of additional configuration files inside the container

file:/etc/app/config/

Yes

TIMEOUT

Default timeout for device operations, in seconds

60

Yes

TRANSACTION_DELAY

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

1000

Yes

LOGGING_LEVEL_COM_FRIENDLY_PROVISIONPORTAL

Log level for the application package. Values: ERROR, WARN, INFO, DEBUG

DEBUG

Yes

SPRING_THREADS_VIRTUAL_ENABLED

Virtual threads switch. Already false in application-oracle.yml; set it explicitly on Oracle deployments

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

No (optional)

Table 3. provision-portal/.env — UI
Variable Description Default Required

FT_PROV_PORTAL_HTTP_PORT

Host port published for the UI (→ 80)

8890

Yes

FT_PROV_PORTAL_HTTPS_PORT

Host port published for the UI over TLS (→ 443)

8893

Yes

Always set FT_PROV_API_HTTP_PORT=8091 in provision-api/.env. The fallback compiled into this repository’s docker/compose.yml is 8085, but on the shared /usr/local/ft-system host 8085 is the port the platform assigns to the Service API (SERVICE_API_PORT, see Environment Reference). Leaving the fallback in place makes the two containers fight over the same host port and one of them fails to start. 8091 is the value the platform assigns to Provision API (PROV_API_HTTP_PORT), and it is the value used throughout this guide.

CMMN job polling is already off in application.yml (flowable.cmmn.async-executor-activate: false), so no environment override is needed for it.

JWT_SECRET / JWT_EXPIRATION are not set anywhere in the shipped environment file templates or in docker/compose.yml. Without them, every deployment of this service signs JWTs with the same hardcoded default baked into application.yml (provision-portal-secret-key-please-change-in-production…​) — anyone who reads the source can forge a valid token against any deployment that has not overridden it. Set JWT_SECRET (64+ random characters, the minimum for HS512) and JWT_EXPIRATION (milliseconds) in the root .env before exposing this service beyond a local test. This is also called out in Production Checklist.

5.3. TLS Keystore (optional)

Provision Portal UI (Nginx) — the primary, already-wired path. compose.yml mounts ${DATA_FOLDER}/provision-portal/ssl:/etc/nginx/ssl:ro; place a certificate and key there (friendly.crt, friendly.key per the FT_DISK bundle, or your own CA-issued pair under matching names expected by the UI’s Nginx config).

Provision API — serves HTTPS on 8443 from a self-signed keystore bundled 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/env files and is suitable for testing only. To use a custom certificate, extend compose.yml yourself: mount a PKCS12 keystore and add the matching Spring Boot overrides under provision-api.environment:

    volumes:
      - ${DATA_FOLDER:-.}/provision-api-conf:/etc/app
      - ${DATA_FOLDER:-.}/provision-api/logs:/app/Log
      - ./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.

6. Deployment

6.1. Startup Dependencies

provision-api has no depends_on on its infrastructure — the ACS database, the Flowable database, and Northbound API live outside this stack, so Compose cannot gate on their health. All of them must be reachable before provision-api starts: Liquibase applies the Flowable schema migrations on first start, and the JDBC pools connect during context initialisation.

provision-portal (the UI), by contrast, does depend on provision-api within this stack — depends_on: provision-api: condition: service_healthy — so it will not start until the backend’s healthcheck passes.

Wait for the external dependencies explicitly before starting:

# 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
until curl -sf http://<northbound-host-ip>:8080/iot-webservice/actuator/health; do sleep 2; done

# Then start the stack
docker compose up -d

6.2. Docker Compose

Click to expand compose.yml (Provision Portal)
version: "3.8"
name: ft-provision-portal

services:
  provision-api:
    image: hub.friendly-tech.com/api/provision-portal:latest
    ports:
      - "${FT_PROV_API_HTTP_PORT:-8091}:8080"
    env_file:
      - .env
      - ./provision-api/.env
    environment:
      PORT: "8080"
      SPRING_PROFILES_ACTIVE: "${SPRING_PROFILES_ACTIVE:-mysql}"
      SPRING_CONFIG_ADDITIONAL_LOCATION: "file:/etc/app/"
      LOGGING_CUSTOM_BASE_PATH: "/app"
      LOGGING_CUSTOM_DIRECTORY: "Log"
      JAVA_TOOL_OPTIONS: "${FT_PROV_API_JAVA_RAM:--XX:InitialRAMPercentage=50 -XX:MaxRAMPercentage=70} -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -Duser.timezone=${TZ:-Europe/Kyiv} -Doracle.jdbc.timezoneAsRegion=false"
    volumes:
      - ${DATA_FOLDER:-.}/provision-api-conf:/etc/app
      - ${DATA_FOLDER:-.}/provision-api/logs:/app/Log
    extra_hosts:
      - "host.docker.internal:host-gateway"
    healthcheck:
      test: [ "CMD", "curl", "-kf", "https://127.0.0.1:8443/prov-portal/actuator/health" ]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 60s
    restart: unless-stopped

  provision-portal:
    depends_on:
      provision-api:
        condition: service_healthy
    image: hub.friendly-tech.com/ui/provision-portal:latest
    ports:
      - "${FT_PROV_PORTAL_HTTP_PORT:-8890}:80"
      - "${FT_PROV_PORTAL_HTTPS_PORT:-8893}:443"
    env_file:
      - .env
      - ./provision-portal/.env
    environment:
      FT_PROV_API_URL: "http://provision-api:8080/prov-portal/"
    volumes:
      - ${DATA_FOLDER:-.}/provision-portal/ssl:/etc/nginx/ssl:ro
      - ${DATA_FOLDER:-.}/provision-portal/nginx/logs:/var/log/nginx
    healthcheck:
      test: [ "CMD", "curl", "-f", "http://127.0.0.1/provision-portal/index.html" ]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped
    stop_grace_period: 45s

Neither service sets container_name: or a custom networks: entry in the shipped file — Compose names the containers ft-provision-portal-provision-api-1 / ft-provision-portal-provision-portal-1 and puts both on its default project network. Prefer docker compose <cmd> <service-name> (e.g. docker compose logs provision-api) over guessing the container name.

6.2.1. Start the Stack

Start Provision Portal only after the ACS database, the Flowable database, and Northbound API accept connections — see Startup Dependencies.

cd /usr/local/ft-system
docker login hub.friendly-tech.com
docker compose up -d
docker compose ps

6.2.2. Remote Debugging (optional)

By default, JDWP remote debugging is disabled, to avoid the CPU cost and security exposure of an externally reachable debug port. To enable it for provision-api, apply compose.debug.yml as an override:

docker compose -f compose.yml -f compose.debug.yml up -d
Click to expand compose.debug.yml
version: "3.8"

# Usage: docker compose -f compose.yml -f compose.debug.yml up -d
# Enables JDWP remote debugging on port 5005

services:
  provision-api:
    ports:
      - "127.0.0.1:${FT_PROV_API_DEBUG_PORT:-5005}:5005"
    environment:
      JAVA_TOOL_OPTIONS: "${FT_PROV_API_JAVA_RAM:--XX:InitialRAMPercentage=50 -XX:MaxRAMPercentage=70} -Duser.timezone=${TZ:-Europe/Kyiv} -Doracle.jdbc.timezoneAsRegion=false -agentlib:jdwp=transport=dt_socket,server=y,address=*:5005,suspend=n"

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. Verification

7.1. Startup Log

docker compose ps
# Expected: both services "Up" / "healthy"

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

The backend’s healthcheck has start_period: 60s, so provision-api legitimately reports starting for up to a minute before turning healthy; provision-portal stays down until it does.

7.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

# Provision Portal UI
curl -s http://localhost:8890/provision-portal/index.html | head -1
# Expected: beginning of the UI's index.html

The Docker healthcheck for provision-api curls https://127.0.0.1:8443/…​; inside the container, where the HTTPS connector is always present. From the host, only the plain-HTTP port (8091 by default) is published unless you added an 8443 mapping yourself — see Network Requirements.

7.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 the UI:

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).

8. 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.

8.1. HTTP / HTTPS

Port Protocol Purpose Exposure

8091 → 8080

HTTP

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

Public

8443

HTTPS

provision-api same endpoints over TLS, bundled self-signed keystore (HTTPS_PORT); the Docker healthcheck uses it

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

8890 → 80

HTTP

Provision Portal UI served by Nginx (FT_PROV_PORTAL_HTTP_PORT)

Public

8893 → 443

HTTPS

Provision Portal UI over TLS, certificate from provision-portal/ssl (FT_PROV_PORTAL_HTTPS_PORT)

Public

127.0.0.1:5005 → 5005

TCP (JDWP)

provision-api remote debugging, only with compose.debug.yml (FT_PROV_API_DEBUG_PORT)

Localhost

When Provision Portal is deployed as part of the full API stack instead of standalone (see Server E), the port variable names differ: PROV_API_HTTP_PORT instead of FT_PROV_API_HTTP_PORT, and PROV_PORTAL_HTTP_PORT / PROV_PORTAL_HTTPS_PORT instead of the FT_-prefixed pair here. The published backend port is the same 8091 in both. The two compose files are independent — do not mix variables between them.

8.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 (shipped default)

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

8.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; used by the Docker healthcheck over HTTPS on 8443

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

Backend version for the UI

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

GET

http://<host>:8890/provision-portal/index.html

UI entry point served by Nginx; used by the UI container’s healthcheck

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.

9. Stack Management

9.1. Logs

docker compose logs -f provision-api        # backend
docker compose logs -f provision-portal     # UI / Nginx

Application log files are also written to the host through the bind mounts: provision-api/logs/ (→ /app/Log, set by LOGGING_CUSTOM_BASE_PATH / LOGGING_CUSTOM_DIRECTORY) and provision-portal/nginx/logs/ (→ /var/log/nginx).

9.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).

9.2. Start, Stop, Restart

Command Description

docker compose up -d

Start both services in the background

docker compose down

Stop and remove containers

docker compose ps

Show container status

docker compose restart provision-api

Restart the API only

docker compose pull

Update both images to their latest versions

docker stats

View resource usage for both containers

provision-portal declares stop_grace_period: 45s, so a down or restart of the UI may take up to 45 seconds before the container is killed.

9.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
docker compose exec provision-portal sh                       # enter the UI container
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.

9.4. Updating Provision Portal

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

# 2. Pull the latest images
docker compose pull

# 3. Recreate both containers with the new images
docker compose up -d

# 4. Verify
curl -s http://localhost:8091/prov-portal/actuator/health
curl -s http://localhost:8890/provision-portal/index.html | head -1

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.

10. Production Checklist

  • JWT secret — not set by the shipped template. Add JWT_SECRET (64+ random characters) and JWT_EXPIRATION to the root .env; see the warning in Environment Configuration. Without it every deployment shares the same hardcoded signing key.

  • Database credentials — replace MYSQL_PASSWORD / ORACLE_PASSWORD and FLOWABLE_MYSQL_PASSWORD / FLOWABLE_ORACLE_PASSWORD in the root .env; the template ships with ftacs / flowable.

  • Northbound API URL — NORTHBOUND_API_URL points at the correct instance, not the <northbound-host> placeholder.

  • TLS certificate (UI) — replace the FT_DISK-provided friendly.crt / friendly.key with a CA-issued certificate if the UI is reachable outside a trusted network.

  • TLS certificate (API) — replace the bundled self-signed keystore.p12 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 the env files: chmod 600 .env provision-api/.env provision-portal/.env.

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

  • Timezone — set TZ in the root .env (defaults to Europe/Kyiv).

  • Resource limits — FT_PROV_API_JAVA_RAM uses percentage-based heap sizing (-XX:MaxRAMPercentage); without a container memory limit these percentages are computed against the host’s total RAM, not the container’s. Add deploy.resources.limits.memory in compose.yml for predictable sizing.

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

11. Troubleshooting

11.1. Container Fails to Start

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

Fix:

  1. Read the error from the logs:

    docker compose logs provision-api
    docker events --filter container=ft-provision-portal-provision-api-1
  2. Confirm the environment file is loaded:

    docker compose exec provision-api env | grep -E 'MYSQL|ORACLE'
  3. Confirm provision-api-conf/config/ and provision-api-conf/xml/ exist and are populated — an empty mount at /etc/app is a common cause.

11.2. Could not resolve placeholder 'XML_PATH'

Symptom: PlaceholderResolutionException: Could not resolve placeholder 'XML_PATH'

Fix:

  1. Confirm provision-api/.env exists next to the compose.yml Compose runs from and contains XML_PATH=file:/etc/app/xml/.

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

11.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-conf/xml/ on the host.

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

11.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 MYSQL_HOST / ORACLE_HOST and credentials in the root .env.

11.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_MYSQL_HOST / FLOWABLE_ORACLE_HOST and credentials in the root .env.

11.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. If Northbound API also runs in Docker, ensure both stacks share a network and NORTHBOUND_API_URL uses the container name instead of localhost.

11.7. UI Cannot Reach the API

Symptom: The UI loads but shows connection or authentication errors; provision-portal never becomes healthy despite provision-api being healthy.

Fix:

  1. Confirm provision-api passed its own healthcheck first — provision-portal will not start otherwise (depends_on: condition: service_healthy): docker compose ps.

  2. Verify FT_PROV_API_URL inside the UI container: docker compose exec provision-portal env | grep FT_PROV_API_URL — it must resolve to http://provision-api:8080/prov-portal/ on the Compose network.

11.8. 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 the published port in the per-service .env (FT_PROV_API_HTTP_PORT in provision-api/.env; FT_PROV_PORTAL_HTTP_PORT, FT_PROV_PORTAL_HTTPS_PORT in provision-portal/.env).

[[401-or-403-from-the-ui-api-calls]] === 401 or 403 from the UI API Calls

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

Cause: JWT_SECRET was set (or changed) and the container restarted — all previously issued tokens are invalidated because they were signed with the old key.

Fix: re-authenticate through the UI. If this happens on every restart even without changing JWT_SECRET, confirm the variable is actually set in the root .env — otherwise the application falls back to its hardcoded default, which does not change between restarts but is also not a real fix; see Production Checklist.

11.9. 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 logs --tail 100 provision-portal
docker compose exec provision-api env | grep -E 'SPRING_PROFILES_ACTIVE|MYSQL_HOST|ORACLE_HOST|FLOWABLE|NORTHBOUND|XML_PATH|CONFIG_PATH'
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.