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 |
|
Provision Portal API uses the Compose project name |
2. Prerequisites
2.1. Host Requirements
| Component | Minimum | Recommended | Notes |
|---|---|---|---|
Docker Engine |
20.10 |
Latest stable |
The stack |
Docker Compose |
2.0 |
Latest stable |
Invoked as |
RAM |
2 GB |
4 GB |
The JVM sizes its heap from the container memory limit; without |
Free disk space |
1 GB |
10 GB |
The image, plus the |
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 |
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 |
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 |
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
|
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) |
|
Two-stage |
Java |
25 |
Language level and runtime ( |
Application framework |
Spring Boot 3.5.7 |
Web layer, JPA, validation, security ( |
Workflow engine |
Flowable 7.2.0 |
BPMN provisioning processes; owns its own database schema ( |
Relational database |
MySQL 8.4 or Oracle 21c |
ACS schema and the separate Flowable schema. Platform-wide minimum |
MySQL driver |
|
JDBC access to MySQL ( |
Oracle driver |
|
JDBC access to Oracle ( |
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 |
|
Flowable database |
3306 (Oracle: 1521) |
TCP |
|
Northbound API |
8080 on the shared bridge; 9880 published across hosts |
TCP |
|
|
8091 → 8080 |
TCP |
REST/SOAP API, Swagger UI, Actuator, |
|
|
|
For a quick connectivity check from any host:
|
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 |
|
Password |
|
|
The |
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 |
-
On the offline host, find out which architecture it runs — this is the value you will pass as
PLATFORMbelow. 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 -mon Linux/macOS, orecho $env:PROCESSOR_ARCHITECTUREin PowerShell on Windows. Map the result:docker versionreportsuname -m/ Windows reportsUse as PLATFORMamd64x86_64/AMD64linux/amd64arm64aarch64/ARM64linux/arm64On 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. -
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 -
Pull the backend image and export it into an archive. Paste the whole block as-is; the only lines to change are
PLATFORMandTAGon 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 redirectingdocker savefrom PowerShell (docker save … > file.tar) corrupts the archive, because the PowerShell pipeline re-encodes the stream as text instead of passing raw bytes.docker loadthen fails withunexpected EOForinvalid tar header. To compress for transfer, use the built-intar.exe(Windows 10 1803+ / Server 2019+):tar.exe -czf provision-api.tar.gz provision-api-$TAG.tar. -
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. -
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-apiWindows (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 |
|---|---|---|
|
Stack definition, including the |
Yes |
|
Shared stack environment — database connection, Flowable database, Northbound API URL, JWT, time zone, and the published ports |
Yes |
|
Per-service environment — container ports, config paths, HikariCP pool sizing, |
Yes |
|
Provisioning XML files ( |
Yes |
|
|
Yes |
|
Backend logs written to |
No |
|
The web interface is deployed from its own FT_DISK folder into the sibling directory |
|
This guide follows the platform layout defined in
Full System Deployment:
the directory is |
|
|
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 |
|
|
|
Shared environment |
FT System MySQL Stack / FT System Oracle Stack |
|
|
Provision Portal API environment |
|
Only one file: the |
|
Provision Portal API configuration |
|
All XML files → |
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 |
|---|---|
|
Database connection ( |
|
Per-service settings: container ports ( |
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 |
|---|---|---|---|
|
Active Spring profile; selects the datasource set and dialect ( |
|
Yes |
|
Container time zone, and therefore the JVM default zone |
|
No |
|
Plain-HTTP connector port inside the container ( |
|
Yes |
|
Main connector port inside the container ( |
|
No |
|
MySQL host for both the ACS and Flowable datasources; falls back to |
|
Yes on the |
|
MySQL port |
|
No |
|
Schema of the ACS datasource |
|
No |
|
User of the ACS datasource |
|
Yes on the |
|
Password of the ACS datasource |
|
Yes on the |
|
Full JDBC URL of the ACS datasource; overrides the URL built from the host, port and schema above |
built from |
No |
|
JDBC driver class |
|
No |
|
|
|
No |
|
Oracle listener host for both datasources; falls back to |
|
Yes on the |
|
Oracle listener port |
|
No |
|
Oracle service name (thin driver) |
|
No |
|
Account owning the ACS schema |
|
Yes on the |
|
Password for |
|
Yes on the |
|
Full JDBC URL of the ACS datasource; overrides the URL built from the host, port and service above |
built from |
No |
|
JDBC driver class |
|
No |
|
Flowable database host (MySQL); falls back to |
|
No |
|
Flowable database port (MySQL); falls back to |
|
No |
|
Flowable schema name (MySQL) |
|
No |
|
Flowable database user (MySQL) |
|
Yes on the |
|
Flowable database password (MySQL) |
|
Yes on the |
|
Flowable database host (Oracle); falls back to |
|
No |
|
Flowable listener port (Oracle); falls back to |
|
No |
|
Flowable service name (Oracle); falls back to |
|
No |
|
Account owning the Flowable schema (Oracle) |
|
Yes on the |
|
Password for |
|
Yes on the |
|
HikariCP maximum pool size for the ACS datasource; also the default for the Flowable pool |
|
No |
|
HikariCP minimum idle connections |
|
No |
|
HikariCP connection timeout in milliseconds |
|
No |
|
Flowable pool overrides; each falls back to the |
inherit |
No |
|
Base URL of the Northbound API instance |
|
Yes |
|
Spring resource path of the provisioning XML directory |
|
Yes |
|
Spring resource path of the additional configuration directory ( |
|
Yes |
|
Default timeout for device operations, in seconds |
|
No |
|
Extra delay after a transaction response, in milliseconds; increase under heavy ACS load |
|
No |
|
HS512 signing key for the tokens issued to the UI REST API; the value in |
|
Yes in production |
|
Token lifetime in milliseconds |
|
No |
|
Location of the provisioning trace files ( |
|
No |
|
Directory of the Spring Boot |
|
No |
|
Log level for |
|
No |
|
Flowable CMMN async job executor switch |
|
No |
|
Virtual threads switch; |
|
No |
|
The sample block below also shows |
# /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
|
|
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
|
|
|
Replace |
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 |
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 |
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 |
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 |
|
On another host |
|
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 |
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 |
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 |
|
Public |
8443 |
HTTPS |
|
Internal (bridge only) — not published by the shipped |
127.0.0.1:5005 → 5005 |
TCP (JDWP) |
|
Localhost |
|
All three published ports are host-level values and live in |
9.2. Outbound Connections
| Destination | Port | Protocol | Purpose |
|---|---|---|---|
ACS database |
3306 (Oracle: 1521) |
TCP |
JDBC connection to the |
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 |
|
443 |
HTTPS |
Image pulls ( |
9.3. HTTP Endpoints
All backend paths are under the servlet context path /prov-portal (application.yml).
| Method | Path | Purpose | Auth |
|---|---|---|---|
GET |
|
Liveness and datasource details |
None |
GET |
|
Prometheus scrape endpoint (micrometer registry) |
None |
POST |
|
UI login, issues the JWT |
None |
GET |
|
Application version |
None |
GET, POST |
|
UI REST API (devices, provisioning, users) |
JWT bearer token |
POST |
|
SOAP provisioning endpoint (CXF servlet mapped at |
None |
GET |
|
WSDL for the SOAP endpoint |
None |
GET |
|
Swagger UI |
None |
GET |
|
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 |
|---|---|---|
|
|
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. |
|
|
The Spring Boot log. Without |
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 |
|---|---|
|
Start the backend in the background |
|
Stop the backend without touching the rest of the stack |
|
Stop and remove every container of the |
|
Show container status |
|
Restart the API only |
|
Update the image to its latest version |
|
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_SECRETwith 64+ random characters and reviewJWT_TOKEN_EXPIREDin 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_PASSWORDplaceholders in the root.env, and reviewDB_USER/FLOWABLE_DB_USER, which ship asftacs/flowable. -
Northbound API URL —
NORTHBOUND_API_URLpoints at the correct instance: the shippedhttp://northbound-api:8080/iot-webserviceresolves only when Northbound API runs in the same stack on this host; across hosts use its address and published port (9880). -
Database hosts —
DB_HOSTandFLOWABLE_DB_HOSTin the root.envpoint at the real database, not at themysql/oraclecompose service when the database is external. -
TLS certificate — replace the self-signed
keystore.p12inside the image if HTTPS on the API is exposed; see TLS Keystore (optional). If not, keep 8443 unpublished. -
API exposure —
SecurityConfigleaves the SOAP and integration REST endpoints unauthenticated (anyRequest().permitAll()); put a reverse proxy in front of 8091 or keep it off public networks. -
CORS —
SecurityConfigcombinesallowedOriginPattern("*")withallowCredentials(true), effectively reflected-origin CORS with credentials. Review before exposing this service beyond a trusted network; this is an application-code change, not something.envcontrols. -
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
TZin the root.env(ships asEurope/Kyiv); it is also the JVM default zone. -
Resource limits — nothing in the stack sets
JAVA_OPTSfor this service, so the JVM sizes its heap from the memory it sees; without a container limit that is the host’s total RAM. Adddeploy.resources.limits.memoryto the service incompose.yml, andJAVA_OPTS: -XX:MaxRAMPercentage=70underenvironment:if the default 25 % is too small. -
Image tag — pin
image:to a release tag; the shippedcompose.ymluseslatest, so adocker compose pullcan move the deployment to a new version unintentionally. -
Debug port — no
compose.debug.ymloverride 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:
-
Read the error from the logs:
docker compose logs provision-api docker events --filter container=provision-api -
Confirm both environment layers are loaded:
docker compose exec provision-api env | grep -E 'MYSQL|ORACLE|TIMEOUT|HTTPS_PORT'Values from the root
.envand fromprovision-api/.envmust both be present — anenv_file:entry pointing at a missing file makes Compose fail outright, but a file in the wrong directory silently leaves the defaults in place. -
Confirm
provision-api/config/andprovision-api/xml/exist and are populated — an empty mount at/etc/app/xmlis a common cause.
12.2. Could not resolve placeholder 'XML_PATH'
Symptom: PlaceholderResolutionException: Could not resolve placeholder 'XML_PATH'
Fix:
-
Confirm the
environment:block of theprovision-apiservice still setsXML_PATH: file:/etc/app/xml/— that is where it comes from, not from either.envlayer. -
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:
-
Confirm the XML files exist under
provision-api/xml/on the host. -
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:
-
Test connectivity from inside the container:
docker compose exec provision-api nc -zv <db-host> <db-port> -
Check
DB_HOST,DB_PORTand the credentials in the root.env— theMYSQL_*/ORACLE_*names inside the container are derived from them by the compose anchors, so fix theDB_*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:
-
Verify the Flowable schema exists (see Flowable Database Setup).
-
Check
FLOWABLE_DB_HOSTand the Flowable credentials in the root.env.
12.6. Northbound API Connection Failure
Symptom: Connection refused or timeout errors when calling Northbound API.
Fix:
-
Verify
NORTHBOUND_API_URLin the root.envis correct and reachable:docker compose exec provision-api curl -sf $NORTHBOUND_API_URL/actuator/health. -
On the same stack,
NORTHBOUND_API_URLshould use the service name (http://northbound-api:8080/iot-webservice). When Northbound API runs elsewhere, use its host address and published port — neverlocalhost, 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:
-
Find the process using the port:
lsof -i :8091 # or docker ps -
Stop the conflicting process, or change
PROV_API_HTTP_PORTin the root.env(see [env-file-is-not-interpolated] for why it lives there and not inprovision-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:
-
Set a container memory limit — add
deploy.resources.limits.memoryto theprovision-apiservice incompose.yml; the JVM sizes its heap from that limit (25 % by default). -
To use more of it, add
JAVA_OPTS: -XX:MaxRAMPercentage=70under theenvironment:block —docker-entrypoint.shpassesJAVA_OPTStojava. -
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.
13. Related Documentation
-
All in one server deployment — deploying the infrastructure this guide assumes.
-
Separate server deployment — Server E — the split topology this service joins, alongside Northbound API and Service API.
-
Java API Stack deployment — Provision Portal together with Northbound API and Service API on one host.
-
XML Configuration — the provisioning XML files mounted at
/etc/app/xml. -
Database Configuration — the two datasources and how they are wired.
-
Configuring the Flowable ProcessEngine — the workflow engine backed by the Flowable schema.
-
Northbound API Integration — what the backend calls through
NORTHBOUND_API_URL. -
UI REST API — the JWT-protected
/ui/**endpoints. -
Provision Portal UI — Installation & Deployment — the web interface, deployed from its own FT_DISK folder.