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 (bootJar → provision-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 |
|---|---|---|
|
|
REST/SOAP backend, Flowable workflow engine, provisioning logic |
|
|
Angular UI served by Nginx; proxies to |
|
The two images live under different Harbor namespaces despite sharing the same base name: the backend is |
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 |
|
Docker Compose |
2.0 |
Latest stable |
Invoked as |
RAM |
2 GB |
4 GB |
|
Free disk space |
1 GB |
10 GB |
Images, plus |
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 |
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 |
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 |
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
|
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 |
|
Flowable database |
3306 (Oracle: 1521) |
TCP |
|
Northbound API |
8080 (shipped default) |
TCP |
|
|
8080 |
TCP |
Nginx proxies UI calls to |
|
8890 (HTTP) / 8893 (HTTPS) |
TCP |
Web interface |
|
8091 → 8080 |
TCP |
REST/SOAP API, Swagger UI, Actuator, |
|
|
|
For a quick connectivity check from any host:
|
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 |
|
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
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 |
-
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 both images and export them into a single 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 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 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 bundledtar.exe(Windows 10 1803+ / Server 2019+):tar.exe -czf provision-portal.tar.gz provision-portal-$TAG.tar. -
Transfer the archive to the offline host, together with
compose.yml, the root.envand the per-serviceprovision-api/.envandprovision-portal/.env, the TLS files inprovision-portal/ssl/(friendly.crt,friendly.key) and the XML files inprovision-api-conf/xml/— 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-portal-$TAG.tar.gz" | docker load docker compose up -dWindows (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 |
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 |
|---|---|---|
|
Stack definition for |
Yes |
|
Optional override that enables JDWP on |
Yes |
|
Shared stack environment — database connection, Flowable database, inter-service URLs, JWT, |
Yes |
|
Per-service environment for the backend — published and in-container ports, JVM options, config paths, timeouts, log level |
Yes |
|
Backend application logs, written to |
No |
|
|
Yes |
|
Provisioning XML files ( |
Yes |
|
Per-service environment for the UI — published HTTP and HTTPS host ports |
Yes |
|
TLS certificate and key for the UI’s Nginx ( |
Yes |
|
Nginx access and error logs |
No |
|
|
|
|
Download the following from FT_DISK:
| Component | FT_DISK | Target directory | Files |
|---|---|---|---|
Provision API — stack definition |
|
|
|
Provision API — provisioning XML |
|
All XML files ( |
|
Provision Portal UI |
|
|
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 |
|---|---|
|
Database connection, Flowable database, inter-service URLs, JWT, |
|
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.
| Variable | Description | Default | Required |
|---|---|---|---|
|
Database profile: |
|
Yes |
|
Base directory on the host for all bind mounts |
|
Yes |
|
Container timezone, also passed to the JVM as |
|
Yes |
|
Timezone used in the MySQL JDBC URL ( |
|
Yes (MySQL) |
|
ACS database host |
|
Yes |
|
ACS database port |
|
Yes |
|
ACS schema name (MySQL) or Oracle service name |
|
Yes |
|
ACS database user |
|
Yes |
|
ACS database password — replace the template value |
|
Yes |
|
Flowable database host (may be the same instance as the ACS database) |
|
Yes |
|
Flowable database port |
|
Yes |
|
Flowable schema name (MySQL) or Oracle service name |
|
Yes |
|
Flowable database user |
|
Yes |
|
Flowable database password — replace the template value |
|
Yes |
|
Base URL of the Northbound API instance |
|
Yes |
|
Signing key for the UI REST API tokens. Not present in the shipped template — see the warning below |
hardcoded fallback in |
No (optional) — set it in production |
|
Token lifetime in milliseconds |
|
No (optional) |
|
|
| Variable | Description | Default | Required |
|---|---|---|---|
|
Backend HTTP port inside the container (the additional Tomcat connector). Keep 8080 — the compose port mapping expects the application here |
|
Yes |
|
Backend HTTPS port inside the container (the main connector, TLS via the bundled classpath keystore). Not published by default |
|
Yes |
|
Host port published for the backend ( |
|
Yes |
|
Host port for JDWP remote debugging; only read when |
|
No (optional) |
|
JVM heap sizing flags for the backend container |
|
Yes |
|
Location of the provisioning XML files inside the container |
|
Yes |
|
Location of additional configuration files inside the container |
|
Yes |
|
Default timeout for device operations, in seconds |
|
Yes |
|
Extra delay after a transaction response, in milliseconds; increase under heavy ACS load |
|
Yes |
|
Log level for the application package. Values: |
|
Yes |
|
Virtual threads switch. Already |
|
No (optional) |
| Variable | Description | Default | Required |
|---|---|---|---|
|
Host port published for the UI ( |
|
Yes |
|
Host port published for the UI over TLS ( |
|
Yes |
|
Always set |
CMMN job polling is already off in application.yml (flowable.cmmn.async-executor-activate: false), so no environment override is needed for it.
|
|
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 |
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 |
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 |
|
Public |
8443 |
HTTPS |
|
Internal (bridge only) — not published by the shipped |
8890 → 80 |
HTTP |
Provision Portal UI served by Nginx ( |
Public |
8893 → 443 |
HTTPS |
Provision Portal UI over TLS, certificate from |
Public |
127.0.0.1:5005 → 5005 |
TCP (JDWP) |
|
Localhost |
|
When Provision Portal is deployed as part of the full API stack instead of standalone (see Server E), the port variable names differ: |
8.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 (shipped default) |
HTTP |
REST calls that dispatch TR-069 provisioning operations |
|
443 |
HTTPS |
Image pulls ( |
8.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; used by the Docker healthcheck over HTTPS on 8443 |
None |
GET |
|
Prometheus scrape endpoint (micrometer registry) |
None |
POST |
|
UI login, issues the JWT |
None |
GET |
|
Backend version for the UI |
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 |
GET |
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 |
|---|---|
|
Start both services in the background |
|
Stop and remove containers |
|
Show container status |
|
Restart the API only |
|
Update both images to their latest versions |
|
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) andJWT_EXPIRATIONto 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_PASSWORDandFLOWABLE_MYSQL_PASSWORD/FLOWABLE_ORACLE_PASSWORDin the root.env; the template ships withftacs/flowable. -
Northbound API URL —
NORTHBOUND_API_URLpoints at the correct instance, not the<northbound-host>placeholder. -
TLS certificate (UI) — replace the FT_DISK-provided
friendly.crt/friendly.keywith a CA-issued certificate if the UI is reachable outside a trusted network. -
TLS certificate (API) — replace the bundled self-signed
keystore.p12if 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 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
TZin the root.env(defaults toEurope/Kyiv). -
Resource limits —
FT_PROV_API_JAVA_RAMuses 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. Adddeploy.resources.limits.memoryincompose.ymlfor predictable sizing. -
Debug port —
compose.debug.ymlis 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:
-
Read the error from the logs:
docker compose logs provision-api docker events --filter container=ft-provision-portal-provision-api-1 -
Confirm the environment file is loaded:
docker compose exec provision-api env | grep -E 'MYSQL|ORACLE' -
Confirm
provision-api-conf/config/andprovision-api-conf/xml/exist and are populated — an empty mount at/etc/appis a common cause.
11.2. Could not resolve placeholder 'XML_PATH'
Symptom: PlaceholderResolutionException: Could not resolve placeholder 'XML_PATH'
Fix:
-
Confirm
provision-api/.envexists next to thecompose.ymlCompose runs from and containsXML_PATH=file:/etc/app/xml/. -
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:
-
Confirm the XML files exist under
provision-api-conf/xml/on the host. -
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:
-
Test connectivity from inside the container:
docker compose exec provision-api nc -zv <db-host> <db-port> -
Check
MYSQL_HOST/ORACLE_HOSTand 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:
-
Verify the Flowable schema exists (see Flowable Database Setup).
-
Check
FLOWABLE_MYSQL_HOST/FLOWABLE_ORACLE_HOSTand credentials in the root.env.
11.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. -
If Northbound API also runs in Docker, ensure both stacks share a network and
NORTHBOUND_API_URLuses the container name instead oflocalhost.
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:
-
Confirm
provision-apipassed its own healthcheck first —provision-portalwill not start otherwise (depends_on: condition: service_healthy):docker compose ps. -
Verify
FT_PROV_API_URLinside the UI container:docker compose exec provision-portal env | grep FT_PROV_API_URL— it must resolve tohttp://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:
-
Find the process using the port:
lsof -i :8091 # or docker ps -
Stop the conflicting process, or change the published port in the per-service
.env(FT_PROV_API_HTTP_PORTinprovision-api/.env;FT_PROV_PORTAL_HTTP_PORT,FT_PROV_PORTAL_HTTPS_PORTinprovision-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.
12. Related Documentation
-
All in one server deployment — deploying the infrastructure this guide assumes.
-
Separate server deployment — Server E — Provision Portal as part of the full API stack, with its own port variable names.
-
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.