Installation & Deployment

This guide covers deploying the Northbound API (NBI) — a Spring Boot 4 / Java 25 REST and SOAP integration layer between carrier back-office systems and FTACS — as a single service.

The infrastructure NBI depends on — the ACS relational database, a Hazelcast cluster, and the FTACS SOAP endpoint — is assumed to be already deployed and reachable; this guide only points NBI at it. To deploy that infrastructure, see All in one server deployment or Separate server deployment — Server C.
To deploy NBI alongside Service API and Provision Portal as one stack, see Java API Stack deployment.
For the full environment variable reference, see Configuration Guide — Environment Variables.

1. Overview

NBI is a single container (northbound-api) exposing REST, SOAP, and Swagger UI under the /iot-webservice context path. It is a plain Compose service on the ft-network bridge, deployed from /usr/local/ft-system/.

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

Carrier back-office / OSS / CRM  ->  Northbound API  ->  MySQL / Oracle (ftacs + iotw schemas)  +  Hazelcast  +  FTACS (SOAP)

NBI uses the Compose project name ft-system and the directory root /usr/local/ft-system/ — the same layout as every deployment guide in the Friendly Tech portal. Keeping the layout identical means the northbound-api service can be added to an existing ft-system stack without moving any directory.

2. Prerequisites

2.1. Host Requirements

Component Minimum Recommended Notes

Docker Engine

20.10

Latest stable

Required for the only supported deployment method

Docker Compose

2.0

Latest stable

The compose.yml in this guide uses Compose v2 syntax

CPU

2 cores

2 cores

Single Spring Boot container

RAM

2 GB

4 GB

JVM heap is sized by -XX:MaxRAMPercentage=75.0 (Dockerfile)

Disk

1 GB

5 GB

Image plus northbound-api/logs/, which grows unbounded without logrotate

Network

100 Mbps

1 Gbps

Interface used for database, Hazelcast and FTACS traffic

JDK

25

25

Only when building from source with Gradle; the Docker deployment needs no JDK on the host (eclipse-temurin:25-jdk-alpine builder, Dockerfile)

2.2. Required External Dependencies

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

Component Minimum Version Why It Is Needed Port Required

MySQL or Oracle

verify with FT DevOps — this repo pins no server version (JDBC drivers only: com.mysql:mysql-connector-j, ojdbc11:23.26.1.0.0, build.gradle)

Holds the ftacs schema (ACS data) and the iotw schema (OneIoT web back-end) read by the second datasource that powers the Group Update API. Without it NBI fails to start — both datasources are created at startup.

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

Yes

FTACS

verify with the FTACS team

NBI calls FTACS over SOAP to execute every device operation. Without it device RPCs fail with a connection error.

8080 (HTTP) by default (ACS_PORT), or the ACS host’s HTTPS port

Yes

Hazelcast

5.6 (client library com.hazelcast:hazelcast:5.6.0, build.gradle)

Distributed cache used by the shared ft-cache module. Without it NBI runs without cached lookups.

5701 (TCP) by default, or the port advertised in hazelcast-client.yaml

No (optional)

Both schemas live on the same server: the iotw datasource reuses the host, port, user and password of the ftacs datasource, only the schema differs (MYSQL_SCHEMA_IOTW, default iotw).

Deploying that infrastructure is out of scope here — see All in one server deployment for a single-host stack, or Separate server deployment — Server C for FTACS, and Server A for the database, on a split topology.

2.3. Supported Operating Systems

Deployment Operating system

Docker

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 the FT_DISK — northbound-api folder for the deployment files (.env, northbound-api/.env) and the pre-configured northbound-api configuration package (api.properties, hazelcast-client.yaml).

3. Network Requirements

Outbound connections NBI opens to its infrastructure, and inbound connections it accepts. Each row is a connection that must be open through firewalls when the peer is on another host; traffic to peers on the same Docker bridge needs no rule.

Destination Port Protocol Purpose

Database (ftacs + iotw schemas)

3306 (MySQL), 1521 (Oracle)

TCP

JDBC connections opened by NBI

Hazelcast

5701

TCP

Hazelcast client connection (optional — only when caching is used)

FTACS

8080, or the ACS host’s HTTPS port

TCP

SOAP calls to FTACS for device operations

Northbound API (inbound from carrier back-office / OSS / CRM, Service API, Provision Portal)

9880 → 8080, and 9444 → 8443 when HTTPS is enabled

TCP

REST, SOAP, Swagger UI, Actuator on the published host ports

Northbound API (inbound from monitoring)

9880 → 8080

TCP

Prometheus scrapes /iot-webservice/actuator/prometheus

For a quick connectivity check from any host:

# Replace with target host IP and port
nc -zv <target-ip> <port>

3.1. Docker Networking

NBI runs on the ft-network bridge defined in compose.yml. Peers on the same bridge are reached by container name and by their container port — no firewall rule and no published port are involved.

The service declares extra_hosts: - "host.docker.internal:host-gateway", so host.docker.internal resolves to the Docker host from inside the container. Use it in DB_HOST or ACS_URL, and in the member addresses in hazelcast-client.yaml, when the peer runs directly on the host rather than in a container.

4. Registry Authentication

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

NBI ships as a single registry image — there is no local build step to transfer. 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 image in Harbor is 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, 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 and export the image. 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/northbound-api:$TAG"
    docker save "hub.friendly-tech.com/api/northbound-api:$TAG" | gzip > "northbound-api-$TAG.tar.gz"

    Windows (PowerShell):

    $PLATFORM = "linux/amd64"
    $TAG = "latest"
    
    docker pull --platform $PLATFORM "hub.friendly-tech.com/api/northbound-api:$TAG"
    docker save -o "northbound-api-$TAG.tar" "hub.friendly-tech.com/api/northbound-api:$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 northbound-api.tar.gz northbound-api-$TAG.tar.

  4. Transfer the archive to the offline host, together with compose.yml, .env, northbound-api/.env, and the files in northbound-api/config/.

  5. On the offline host, load the archive and start the service. Set TAG to the same value you used above:

    Linux (bash):

    TAG=latest
    
    gzip -dc "northbound-api-$TAG.tar.gz" | docker load
    docker compose up -d northbound-api

    Windows (PowerShell):

    $TAG = "latest"
    
    docker load -i "northbound-api-$TAG.tar"
    docker compose up -d northbound-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/northbound-api

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

This covers the NBI image only. The database, Hazelcast, and FTACS are separate images on their own hosts — see All in one server deployment — Offline Servers for the procedure covering a whole stack.

5. Preparation

Create the directories NBI needs on the host and point it at the infrastructure listed in Prerequisites.

5.1. Directory Structure

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

5.1.1. Directory Layout

Once the files below are in place, the NBI slice of /usr/local/ft-system/ looks like this:

/usr/local/ft-system/
├── compose.yml                             # from FT_DISK
├── .env                                    # shared stack environment
└── northbound-api/
    ├── .env                                # per-service environment
    ├── config/                             # -> /etc/app (mounted read-only)
    │   ├── api.properties                  # from FT_DISK -> northbound-api
    │   ├── hazelcast-client.yaml           # from FT_DISK -> northbound-api
    │   └── keystore.p12                    # only when HTTPS is enabled
    └── logs/                               # -> /app/logs
Path Content Backup

compose.yml

Stack definition, including the northbound-api service block

Yes

.env

Shared stack environment — database connection, FTACS, Hazelcast, time zone

Yes

northbound-api/.env

Per-service environment — container ports, HikariCP pool, config paths, JWT, logging

Yes

northbound-api/config/

api.properties, hazelcast-client.yaml and the optional keystore.p12, mounted read-only at /etc/app

Yes

northbound-api/logs/

Application logs written by the container at /app/logs; grows unbounded without logrotate

No

This guide follows the platform layout defined in Full System Deployment: the directory is northbound-api/, the configuration files live in its config/ subdirectory, and that subdirectory is mounted read-only at /etc/app. The docker/compose.yml shipped in this repository is a developer stack and still mounts ./nbi-api:/etc/app — the whole directory, read-write. That difference is recorded in docs/decisions/LOG.md; if your compose.yml came from that repository rather than from FT_DISK, check its volumes: and env_file: entries and adjust the paths accordingly.

Download the following from FT_DISK or the source repository (docker/ for .env, src/main/resources/ for api.properties, src/main/resources/config/ for hazelcast-client.yaml):

Component FT_DISK Target directory Files

NBI environment

FT_DISK — northbound-api

northbound-api/.env

Only one file: the .env in the folder root

NBI configuration

FT_DISK — northbound-api

northbound-api/config/

api.properties, hazelcast-client.yaml

The northbound-api/config/ directory is mounted read-only at /etc/app inside the container (./northbound-api/config:/etc/app:ro), and northbound-api/logs/ is mounted at /app/logs (./northbound-api/logs:/app/logs):

File Purpose

api.properties

API behavioral settings — logging, timeouts, device handling, authentication, file upload

hazelcast-client.yaml

Hazelcast cache cluster client configuration

keystore.p12

SSL keystore for the HTTPS port (only when HTTPS is enabled — see TLS Keystore (optional))

See Configuration Guide for the reference on each configuration item.

5.2. Environment Configuration

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

File Purpose

.env

Database connection (DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_PROFILE), SPRING_PROFILES_ACTIVE, TZ, Hazelcast (HZ_MEMBERS) and FTACS (ACS_URL, ACS_PORT, ACS_USERNAME, ACS_PASSWORD). Shared by every service on the host.

northbound-api/.env

Per-service settings: container ports (PORT, HTTPS_PORT), TLS (SERVER_SSL_*), config paths (API_CONFIG_PATH, CACHE_CONFIG_PATH, API_DEVICE_TEMPLATES_PATH), the OneIoT schema (MYSQL_SCHEMA_IOTW, ORACLE_USER_IOTW, ORACLE_PASSWORD_IOTW), HikariCP tuning (DB_MAX_POOL_SIZE, DB_MIN_IDLE, DB_CONNECTION_TIMEOUT_MS), the SQL login queries, JWT_TOKEN_*, TIMEOUT and LOGGING_LEVEL_COM_FRIENDLY_NORTHBOUNDAPI.

Both files are loaded by the env_file: list of the northbound-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_* and ORACLE_* variables in the table below are not written by hand: compose.yml derives them from the generic DB_* values through the x-db-env anchor (MYSQL_HOST: ${DB_HOST} and so on), so one database block in the root .env serves every service.

Every variable below is read by NBI 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 templates shipped on FT_DISK.

Variable Description Default Required

SPRING_PROFILES_ACTIVE

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

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

Yes

TZ

Container time zone; must match the zone in which the ACS stores its created timestamps

UTC

No

PORT

HTTP connector port inside the container (server.port)

8080

No

HTTPS_PORT

HTTPS connector port inside the container; used only when SERVER_SSL_ENABLED=true (HttpsConfiguration.java)

8443

No

SERVER_SSL_ENABLED

Enables the HTTPS connector

false

No

SERVER_SSL_KEY_STORE

Path to the PKCS12 keystore serving the HTTPS port

empty; file:/etc/app/keystore.p12 in the shipped .env

Yes when HTTPS is enabled

SERVER_SSL_KEY_STORE_PASSWORD

Keystore password

empty

Yes when HTTPS is enabled

SERVER_SSL_KEY_STORE_TYPE

Keystore format

PKCS12

No

SERVER_SSL_KEY_PASSWORD

Private key password, when it differs from the keystore password

empty

No

SERVER_SSL_KEY_ALIAS

Alias of the key entry used for TLS

server

No

SERVER_SSL_TRUST_STORE

Trust store used for outbound TLS verification

empty

No

SERVER_SSL_TRUST_STORE_PASSWORD

Trust store password

empty

No

MYSQL_HOST

MySQL host for both the ftacs and iotw datasources; falls back to DB_HOST, then localhost

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

Yes on the mysql profile

MYSQL_PORT

MySQL port

3306

No

MYSQL_SCHEMA

Schema of the ACS datasource

ftacs

No

MYSQL_SCHEMA_IOTW

Schema of the second (OneIoT) datasource that powers the Group Update API

iotw

No

MYSQL_USER

User for both MySQL datasources

ftacs

Yes on the mysql profile

MYSQL_PASSWORD

Password for both MySQL datasources

ftacs

Yes on the mysql profile

MYSQL_DRIVER_CLASS_NAME

JDBC driver class

com.mysql.cj.jdbc.Driver

No

ORACLE_HOST

Oracle listener host; falls back to DB_HOST, then localhost

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

Yes on the oracle profile

ORACLE_PORT

Oracle listener port

1521

No

ORACLE_SERVICE

Oracle service name (thin driver)

XEPDB1

No

ORACLE_USER

Account owning the ACS schema

ftacs

Yes on the oracle profile

ORACLE_PASSWORD

Password for ORACLE_USER

ftacs

Yes on the oracle profile

ORACLE_USER_IOTW

Account owning the iotw schema

iotw

No

ORACLE_PASSWORD_IOTW

Password for ORACLE_USER_IOTW

falls back to ORACLE_PASSWORD

No

ORACLE_DRIVER_CLASS_NAME

JDBC driver class

oracle.jdbc.OracleDriver

No

DB_MAX_POOL_SIZE

HikariCP maximum pool size, applied to both datasources

10

No

DB_MIN_IDLE

HikariCP minimum idle connections

5

No

DB_CONNECTION_TIMEOUT_MS

HikariCP connection timeout in milliseconds

30000

No

ACS_URL

Base URL of the FTACS SOAP endpoint, including the scheme

http://127.0.0.1 in the shipped .env

Yes

ACS_PORT

Port of the FTACS SOAP endpoint

empty; 8080 in the shipped .env

Yes

ACS_USERNAME

FTACS web service account

user in the shipped .env

Yes

ACS_PASSWORD

Password of the FTACS web service account

strong-password in the shipped .env

Yes

API_CONFIG_PATH

Spring resource path to api.properties; NBI does not start without it

file:/etc/app/api.properties in the shipped .env

Yes

CACHE_CONFIG_PATH

Directory holding hazelcast-client.yaml

file:/etc/app/ in the shipped .env

Yes

API_DEVICE_TEMPLATES_PATH

Spring resource path to the device template directory

classpath:device_template/

No

SQL_LOGIN_BY_NAME_AND_LOCATION0

Login query for the global domain scope

no default; set in the shipped .env for the legacy admin.login stack

Yes

SQL_LOGIN_BY_NAME_AND_LOCATIONS

Login query for a list of allowed domain ids

no default; set in the shipped .env for the legacy admin.login stack

Yes

SQL_LOGIN_BY_NAME_NULL_OR_0

Login query treating an unset domain as global

no default; set in the shipped .env for the legacy admin.login stack

Yes

JWT_TOKEN_SECRET

HMAC-SHA256 signing key for JWT tokens; the shipped value is a development default

no default; friendly-tech-jwt-secret-key-2025 in the shipped .env

Yes

JWT_TOKEN_EXPIRED

Token lifetime in milliseconds

no default; 36000000 in the shipped .env

Yes

LOGGING_LEVEL_COM_FRIENDLY_NORTHBOUNDAPI

Log level for com.friendly.northboundapi

DEBUG in the shipped .env

No

The sample blocks below also show DB_PORT, DB_USER, DB_PASSWORD, DB_PROFILE, HZ_MEMBERS and TIMEOUT. NBI reads only DB_HOST directly (as the fallback host in application-mysql.yml / application-oracle.yml). DB_PORT, DB_USER, DB_PASSWORD and DB_PROFILE reach the application through the x-db-env anchor in compose.yml, which maps them to the MYSQL_* / ORACLE_* names the application expects. HZ_MEMBERS and TIMEOUT belong to the platform environment layout (Northbound API Environment Variables) and are kept in the samples so this stack matches it, but no application*.yml in this repository resolves either name. The Hazelcast member list this build actually uses comes from hazelcast-client.yaml (located through CACHE_CONFIG_PATH), and device and ACS timeouts come from api.properties (api.settings.timeout.*). Change either behaviour in those files, and verify with FT DevOps before relying on HZ_MEMBERS or TIMEOUT alone.

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

# Database (existing) -- MySQL profile
DB_HOST=<db-host-ip>                       # <-- replace; use the container name if it shares this bridge
DB_PORT=3306
DB_USER=ftacs
DB_PASSWORD=<your-db-password>             # <-- replace
DB_PROFILE=mysql
SPRING_PROFILES_ACTIVE=mysql

# Hazelcast (existing, optional)
HZ_MEMBERS=<hazelcast-host-ip>:5701        # <-- replace

# FTACS (existing)
ACS_URL=http://<acs-host-ip>               # <-- replace; https:// if ACS is exposed via HTTPS
ACS_PORT=8080                              # 8080 (HTTP) or 443/8443 (HTTPS)
ACS_USERNAME=<acs-user>                    # <-- replace
ACS_PASSWORD=<acs-password>                # <-- replace (get from FT Configs UI: <host>:<port>/acs/ftacsws-access)

TZ MUST match the time zone in which the ACS stores its created timestamps — the same zone as the ftacs service. NBI builds the POST /api/Device/Activity and POST /api/Device/History query window from its JVM default zone, so a TZ that differs from the ACS storage zone shifts the window off the stored timestamps and those endpoints can return an empty list for a device that does have recent activity (DEV-2433). The image default UTC is only correct when the ACS also stores its timestamps in UTC. A running container must be recreated, not just restarted, for a TZ change to take effect.

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 stay as they are). compose.yml maps them to the ORACLE_* names the application expects, so no per-vendor environment file is involved. See Switching to Oracle for the full variable list, and Configuration Guide — Oracle Profile for the datasource details.

Create northbound-api/.env with the per-service variables documented in Configuration Guide — Environment Variables; the full platform-wide reference is Northbound API Environment Variables.

Click to expand a commented northbound-api/.env reference
# =============================================================================
# Northbound API
# =============================================================================

# Container connector ports. The host ports are set in the ports: mapping of
# compose.yml (9880 -> 8080, and 9444 -> 8443 when HTTPS is enabled), not here.
# PORT is the HTTP connector port inside the container.
PORT=8080

# HTTPS connector port inside the container. Only used when SERVER_SSL_ENABLED=true;
# it must differ from PORT so both connectors can bind.
# HTTPS_PORT=8443

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

# Path to api.properties -- contains API behavioral settings (logging, timeouts,
# device handling, authentication, file upload). Required for NBI to start.
API_CONFIG_PATH=file:/etc/app/api.properties

# Path to Hazelcast client configuration directory
CACHE_CONFIG_PATH=file:/etc/app/

# =============================================================================
# HikariCP Connection Pool
# =============================================================================
DB_MAX_POOL_SIZE=10
DB_MIN_IDLE=5
DB_CONNECTION_TIMEOUT_MS=30000

# =============================================================================
# Group Update -- OneIoT (iotw) Datasource
# The Group Update API reads condition views from the OneIoT (iotw) schema
# through a second datasource. The main DB host/port/credentials come from
# root .env; only the items below are NBI-specific.
# =============================================================================

# MySQL: schema name of the OneIoT database. Same host/port/user/password as
# the main ftacs datasource -- only the schema differs. Default: iotw
MYSQL_SCHEMA_IOTW=iotw

# Oracle: the iotw schema is identified by a dedicated account. Set these only
# when running the Oracle profile (each falls back to ORACLE_USER/ORACLE_PASSWORD).
# ORACLE_USER_IOTW=iotw
# ORACLE_PASSWORD_IOTW=<your-iotw-password>         # <-- replace

# =============================================================================
# HTTPS/TLS (optional)
# HTTP and HTTPS can run together when they use different container ports.
# SERVER_SSL_ENABLED=false -> HTTP only.
# SERVER_SSL_ENABLED=true  -> HTTPS on HTTPS_PORT (e.g. 8443).
# In dual mode, keep PORT different from HTTPS_PORT.
# =============================================================================
SERVER_SSL_ENABLED=false
# SERVER_SSL_KEY_STORE=file:/etc/app/keystore.p12
# SERVER_SSL_KEY_STORE_PASSWORD=                    # <-- replace if enabled
# SERVER_SSL_KEY_STORE_TYPE=PKCS12
# SERVER_SSL_KEY_PASSWORD=
# SERVER_SSL_KEY_ALIAS=server
# SERVER_SSL_TRUST_STORE=
# SERVER_SSL_TRUST_STORE_PASSWORD=

# =============================================================================
# SQL Login Queries
# Choose ONE set depending on which portal stack is deployed.
# Current Java/Spring stack (Support Portal, Management Portal) -- iotw_user table.
# Legacy C# stack (CSR, CPEAdmin) -- admin.login table.
# =============================================================================
SQL_LOGIN_BY_NAME_AND_LOCATION0="SELECT password, domain_id FROM iotw_user WHERE username = :name AND domain_id = 0"
SQL_LOGIN_BY_NAME_AND_LOCATIONS="SELECT password, domain_id FROM iotw_user WHERE username = :name AND domain_id IN (:ids)"
SQL_LOGIN_BY_NAME_NULL_OR_0="SELECT password, domain_id FROM iotw_user WHERE username = :name AND (domain_id IS NULL OR domain_id = 0)"

# -- Legacy C# stack (csr / cpeAdmin portals) -- admin.login table --
# SQL_LOGIN_BY_NAME_AND_LOCATION0="SELECT password, location_id FROM admin.login WHERE name = :name AND location_id = 0"
# SQL_LOGIN_BY_NAME_AND_LOCATIONS="SELECT password, location_id FROM admin.login WHERE name = :name AND location_id IN (:ids)"
# SQL_LOGIN_BY_NAME_NULL_OR_0="SELECT password, location_id FROM admin.login WHERE name = :name AND (location_id IS NULL OR location_id = 0)"

# =============================================================================
# Authentication (JWT)
# =============================================================================
# Secret key used to sign and verify JWT tokens (HMAC-SHA256).
# Minimum 32 characters. Must be unique per environment (dev / staging / prod).
# The default value is for development only -- never use it in production.
# Generate a secure value: openssl rand -base64 48
JWT_TOKEN_SECRET=friendly-tech-jwt-secret-key-2025
# Token expiration time in milliseconds (36000000 = 10 hours)
JWT_TOKEN_EXPIRED=36000000

# =============================================================================
# Device Operation Timeout
# =============================================================================
# Max wait time (seconds) for device RPC operations (GetParameterValues,
# SetParameterValues, Download, etc.), per the platform environment reference.
# This build resolves its device and ACS timeouts from api.properties
# (api.settings.timeout.*) -- see the note above.
TIMEOUT=60

# =============================================================================
# Logging
# =============================================================================
# Log level for the application package (com.friendly.northboundapi).
# Values: ERROR, WARN, INFO, DEBUG. In DEBUG mode business errors include stack traces.
# Can also be changed at runtime via Actuator -- see Configuration Guide.
LOGGING_LEVEL_COM_FRIENDLY_NORTHBOUNDAPI=DEBUG

5.3. TLS Keystore (optional)

NBI serves its HTTPS port from a PKCS12 keystore, keystore.p12. To enable HTTPS:

  1. Generate the keystore (or import an existing certificate into one):

    keytool -genkeypair \
      -alias server \
      -keyalg RSA \
      -keysize 2048 \
      -storetype PKCS12 \
      -keystore keystore.p12 \
      -validity 3650 \
      -storepass <keystore-password> \
      -keypass <key-password> \
      -dname "CN=localhost, OU=Dev, O=Friendly, L=Local, ST=Local, C=US" \
      -ext "SAN=dns:localhost,ip:127.0.0.1"
  2. Place keystore.p12 in northbound-api/config/ (mounted read-only at /etc/app).

  3. Set SERVER_SSL_ENABLED=true, HTTPS_PORT=8443, SERVER_SSL_KEY_STORE=file:/etc/app/keystore.p12, SERVER_SSL_KEY_STORE_PASSWORD, and SERVER_SSL_KEY_PASSWORD in northbound-api/.env.

  4. Uncomment the 9444:8443 line in the ports: mapping of compose.yml, then recreate the container — the HTTPS port is not published by default.

ACS_URL / ACS_PORT configure the connection to FTACS and are independent of this keystore — use http:// + 8080 for an HTTP ACS, or https:// + 443/8443 for an HTTPS ACS. Replacing the keystore requires a restart; it is not hot-reloaded.

For production, use a certificate issued by your CA/security team, exported to PKCS12 format.

6. Deployment

6.1. Startup Dependencies

NBI has no depends_on on its infrastructure — the database, Hazelcast, and FTACS live outside this stack, so Compose cannot gate on their health. All of them must be reachable before NBI launches.

Wait for each of them explicitly before starting the service:

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

# Wait for Hazelcast (if caching is used)
until nc -z <hazelcast-host-ip> 5701; do sleep 2; done

# Wait for FTACS
until curl -sf http://<acs-host-ip>:8080/rest/swagger-ui/index.html; do sleep 2; done

# Then start NBI
docker compose up -d northbound-api

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

6.2. Docker Compose

Click to expand compose.yml (Northbound 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

services:

  northbound-api:
    image: hub.friendly-tech.com/api/northbound-api:latest
    container_name: northbound-api
    # The database, Hazelcast and FTACS are external to this stack, so there
    # is no depends_on. They must be reachable before northbound-api starts --
    # DB_HOST and ACS_URL in .env, and the member list in
    # hazelcast-client.yaml, point at them.
    env_file:
      - .env
      - ./northbound-api/.env
    environment:
      <<: *db-env
      API_CONFIG_PATH: file:/etc/app/api.properties
      CACHE_CONFIG_PATH: file:/etc/app/
    ports:
      - "9880:8080"          # HTTP
      # - "9444:8443"        # Optional: publish when HTTPS is enabled
    volumes:
      - ./northbound-api/config:/etc/app:ro
      - ./northbound-api/logs:/app/logs
    extra_hosts:
      - "host.docker.internal:host-gateway"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/iot-webservice/actuator/health"]
      interval: 30s
      timeout: 3s
      start_period: 60s
      retries: 3
    restart: unless-stopped
    networks:
      - ft-network

networks:
  ft-network:
    driver: bridge

For Oracle, the env_file: list stays the same — only the x-db-env anchor changes to Oracle-specific mappings, and the DB_* values are updated in the root .env (see [oracle-profile] and Switching to Oracle):

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}

The 9444:8443 mapping is only meaningful once SERVER_SSL_ENABLED=true — it is commented out in the reference compose (docker/compose.yml in the source repository), so an HTTP-only deployment publishes 9880 only.

6.2.1. Start the Stack

Start NBI only after the database, and Hazelcast (if used), accept connections, and FTACS answers — see Startup Dependencies.

cd /usr/local/ft-system
docker login hub.friendly-tech.com
docker compose up -d northbound-api
docker compose ps
# NBI takes a few seconds to a couple of minutes to start
docker compose logs -f northbound-api | grep -i "started"
curl -s http://localhost:9880/iot-webservice/actuator/health

To run NBI as a single container without Compose:

cd /usr/local/ft-system

docker run -d \
  --name northbound-api \
  --restart unless-stopped \
  --env-file .env --env-file ./northbound-api/.env \
  -p 9880:8080 \
  -p 9444:8443 \
  -v ./northbound-api/config:/etc/app:ro \
  -v ./northbound-api/logs:/app/logs \
  --add-host=host.docker.internal:host-gateway \
  hub.friendly-tech.com/api/northbound-api:latest

docker run publishes only the two ports shown above. Drop -p 9444:8443 for an HTTP-only deployment — see Port Reference.

7. Verification

7.1. Startup Log

Wait a few seconds after start, then check the logs:

# Docker Compose
docker compose logs northbound-api | tail -20

# Docker standalone
docker logs northbound-api | tail -20

A successful startup ends with the StartupInfoLogger summary banner (timestamp, PID, version, build, and Java runtime vary; the active profile and connector reflect your deployment):

INFO --- [northbound-api] [main] c.f.n.config.StartupInfoLogger :
============================================================
Northbound API startup summary
------------------------------------------------------------
Active profiles  : <active-profile>
Connectors       : http (:8080)
Context path     : /iot-webservice
Swagger UI       : /iot-webservice/swagger-ui/index.html
SOAP Endpoint    : /iot-webservice/FTACSWS/ACSWS
SOAP WSDL        : /iot-webservice/FTACSWS/ACSWS?wsdl
API version      : <version>
API build        : <build>
Java runtime     : <java-version>
PID              : <pid>
============================================================

7.2. Endpoint Checks

# Health check
curl -s http://localhost:9880/iot-webservice/actuator/health
# Expected: {"status":"UP"}

# HTTPS health check (when SERVER_SSL_ENABLED=true)
curl -ks https://localhost:9444/iot-webservice/actuator/health

# Swagger UI
curl -I http://localhost:9880/iot-webservice/swagger-ui/index.html

# SOAP WSDL
curl -s http://localhost:9880/iot-webservice/FTACSWS/ACSWS?wsdl | head -5

The health check and Swagger UI must return HTTP 200. The web interface is reachable at http://localhost:9880 (and https://localhost:9444 when HTTPS is enabled and that mapping is uncommented).

7.3. Database Connectivity

NBI does not run its own schema migrations — it connects to the ftacs and iotw schemas that FTACS and UI Backend already manage. Confirm connectivity from inside the container:

docker exec northbound-api nc -zv <db-host-ip> 3306

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. The published ports are the literal values in the ports: mapping of the compose file above; the container ports come from PORT (default 8080) and HTTPS_PORT (default 8443).

8.1. HTTP / HTTPS

Port Protocol Purpose Exposure

9880 → 8080

HTTP

REST, SOAP, Swagger UI and Actuator; container port set by PORT

Public (back-office clients)

9444 → 8443

HTTPS

Same endpoints over TLS, only when SERVER_SSL_ENABLED=true; the mapping is commented out in the reference compose, container port set by HTTPS_PORT

Public (back-office clients) once published

8.2. Outbound Connections

Ports NBI dials on the existing infrastructure. These are the published ports of those services — confirm them against whoever operates them, and set the matching variables in .env.

Destination Port Protocol Purpose

Relational database (ftacs + iotw schemas)

3306 (MySQL), 1521 (Oracle)

TCP

JDBC connections; set through DB_HOST and DB_PORT

Hazelcast cluster (optional)

5701

TCP

Cache client connection; member addresses come from hazelcast-client.yaml (see the note in Environment Configuration about HZ_MEMBERS)

FTACS SOAP endpoint

8080, or 443/8443 for HTTPS

TCP

Device operations over SOAP; set through ACS_URL and ACS_PORT

8.3. HTTP Endpoints

Method Path Purpose Auth

GET

/iot-webservice/actuator/health

Health check used by the container healthcheck

None

GET

/iot-webservice/actuator/prometheus

Prometheus metrics scrape endpoint

None

GET

/iot-webservice/swagger-ui/index.html

Swagger UI (REST + SOAP)

None

GET

/iot-webservice/FTACSWS/ACSWS?wsdl

SOAP web service WSDL

None

The security filter chain permits every request (SecurityConfig.java), so these endpoints are open on whatever interface the ports are published on. Restrict access at the firewall. For how the REST and SOAP operations themselves are authenticated, see API & Integration — Authentication.

All endpoints are exposed under the /iot-webservice servlet context path, which is fixed and not configurable per-endpoint.

9. Stack Management

9.1. Logs

# Follow logs
docker compose logs -f northbound-api

# Last 100 lines
docker compose logs --tail 100 northbound-api

The application writes to northbound-api/logs/ on the host (mounted at /app/logs inside the container).

9.2. Start, Stop, Restart

cd /usr/local/ft-system

docker compose ps                        # status of every service
docker compose stop northbound-api       # stop the service
docker compose start northbound-api      # start it again
docker compose restart northbound-api    # restart it
docker compose down                      # stop and remove all containers

9.3. Shell Access

docker exec -it northbound-api sh
docker exec northbound-api ls -la /etc/app

9.4. Updating Northbound API

cd /usr/local/ft-system

# 1. Back up the NBI configuration and logs
tar -czf northbound-api-backup-$(date +%Y%m%d).tar.gz ./northbound-api

# 2. Update the image tag in compose.yml, then:
docker compose pull northbound-api
docker compose up -d northbound-api

# 3. Verify
curl -s http://localhost:9880/iot-webservice/actuator/health

Configuration files and logs are preserved across the update.

10. Production Checklist

  • Change all default passwords in .env / northbound-api/.env (DB_PASSWORD, ACS_PASSWORD, SERVER_SSL_KEY_STORE_PASSWORD).

  • Replace the default JWT secret — JWT_TOKEN_SECRET ships with a development value; generate a secure one with openssl rand -base64 48.

  • Use a valid SSL certificate — replace any self-signed keystore.p12 with a CA-issued certificate.

  • Configure firewall rules — allow only the required ports; restrict database access to NBI’s host only.

  • Select the correct SQL Login Queries set — current Java/Spring stack (iotw_user) or legacy C# stack (admin.login); comment out the one not in use.

  • Secure configuration files — restrict permissions: chmod 600 northbound-api/.env.

  • Enable auto-restart — restart: unless-stopped in compose.yml, or --restart unless-stopped with docker run.

  • Verify the health check — the compose file defines it; add --health-cmd for standalone docker run.

  • Set up monitoring — deploy Prometheus and Grafana against /iot-webservice/actuator/prometheus.

  • Configure log rotation — northbound-api/logs/ grows unbounded without external logrotate.

  • Set resource limits — add --memory=2g or deploy.resources.limits in compose.yml for production.

11. Troubleshooting

11.1. Container Fails to Start

Symptom: The northbound-api container exits immediately after docker compose up.

Fix:

  1. Read the error message from the logs:

    docker compose logs northbound-api
    docker events --filter container=northbound-api
  2. Confirm the environment files are loaded:

    docker exec northbound-api env | grep -E "DB_|MYSQL_|ACS_"
  3. Confirm northbound-api/config/ exists and is populated — an empty mount at /etc/app is a common cause of Could not resolve placeholder 'API_CONFIG_PATH'.

11.2. Database Connection Failure

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

Fix:

  1. Test connectivity from inside the container:

    docker exec northbound-api nc -zv <db-host-ip> 3306
  2. Verify DB_HOST, DB_PORT, DB_USER, and DB_PASSWORD in .env match the database configuration, and that the container can reach MYSQL_SCHEMA_IOTW (default iotw) on the same host.

11.3. FTACS Connection Failure

Symptom: Device operations fail with a SOAP fault or connection timeout; server.log shows a connection refused/timeout to the ACS host.

Fix:

  1. Verify FTACS answers from inside the container:

    docker exec northbound-api curl -s http://<acs-host-ip>:8080/rest/swagger-ui/index.html
  2. Confirm ACS_URL and ACS_PORT in .env point at the correct scheme (http:// or https://) and port for how FTACS is exposed.

  3. Confirm ACS_USERNAME / ACS_PASSWORD match a valid FTACS web service account (FT Configs UI: <host>:<port>/acs/ftacsws-access).

11.4. Hazelcast Connection Failure

Symptom: Unable to connect to any address in logs (only relevant when Hazelcast caching is used).

Fix:

  1. Verify Hazelcast members are running and reachable on port 5701.

  2. Check that cluster-name and cluster-members in hazelcast-client.yaml match the server configuration.

  3. Confirm CACHE_CONFIG_PATH in northbound-api/.env points to the mounted /etc/app/ directory.

11.5. Could not Resolve Placeholder 'API_CONFIG_PATH'

Symptom: PlaceholderResolutionException: Could not resolve placeholder 'API_CONFIG_PATH'.

Fix:

  1. Verify API_CONFIG_PATH=file:/etc/app/api.properties is set in northbound-api/.env.

  2. Confirm api.properties exists in northbound-api/config/ and the volume mount (./northbound-api/config:/etc/app:ro) is in place.

11.6. Port Already in Use

Symptom: docker compose up fails with bind: address already in use.

Fix:

  1. Find the process holding the port:

    sudo lsof -i :9880
    netstat -tulpn | grep 9880
  2. Either stop that process, or change the published port in the ports: mapping of compose.yml and run docker compose up -d again.

11.7. Permission Denied on Mounted Volumes

Symptom: The container cannot write to northbound-api/logs.

Fix:

sudo chown -R $(whoami):$(whoami) /usr/local/ft-system/northbound-api
chmod -R 755 /usr/local/ft-system/northbound-api

11.8. Out of Memory

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

Fix:

  1. Raise the container memory limit (Docker Desktop: Settings → Resources → Memory), or add deploy.resources.limits.memory in compose.yml.

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

11.9. Build Fails: Permission Denied: ./gradlew

Symptom: docker buildx build from source fails on ./gradlew (developers building from source only — not applicable to a registry image pull).

Fix:

  1. On the host: chmod +x gradlew.

  2. Ensure your source checkout preserves execute permissions.

11.10. Getting Support

If the issue persists, collect the following and contact Friendly Tech support:

  1. .env and northbound-api/.env with passwords masked.

  2. northbound-api/logs/ — the last 1000 lines.

  3. java -version output, if building from source.

  4. Operating system version.

  5. Database type and version.

  6. docker version output.