Installation & Deployment

Overview

This guide walks you through installing and running the Northbound API — a Spring Boot 3 application built with Java 25 and Gradle. Docker is the recommended deployment method for consistency and ease of use.

The deployment includes:

  • Northbound API - Spring Boot REST/SOAP service exposing device management endpoints

  • Docker container - Pre-built image from Harbor registry or loaded from archive

Prerequisites

System Requirements

  • Docker Engine 20.10+

  • Docker Compose 2.0+

  • Minimum 2 GB RAM (4+ GB recommended)

  • 1 GB free disk space for the image and logs

JDK 25 is only required for local Gradle builds. Docker deployment does not need a JDK on the host machine.

Required External Components

The following components must be installed and accessible:

Component Purpose Default Port

MySQL or Oracle

ACS database (schema must already exist)

3306 (MySQL) / 1521 (Oracle)

Hazelcast

Distributed cache (if caching is used)

5701

Supported Operating Systems

  • Linux (recommended)

  • macOS

  • Windows with WSL2

Before starting, make sure:

  • Docker Engine 20.10+ and Docker Compose 2.0+ installed. If Docker is not installed, follow the Docker Installation Guide.

  • You have network access to the ACS database (MySQL or Oracle).

  • You have network access to the Hazelcast cluster (if caching is used).

Quick Start

For experienced users who already have Docker installed and configuration files ready. The example below uses MySQL; for Oracle replace .env.mysql with .env.oracle.

# 1. Go to the working directory (files should already be in place -- see "Preparation")
cd /usr/local/nbi-api

# 2. Get the Docker image (choose one):

#    Option A: Pull from Harbor registry (recommended)
docker login hub.friendly-tech.com
#    Note: "docker pull" is optional if you use docker compose --
#    "docker compose up" will pull the image automatically.
docker pull hub.friendly-tech.com/api/northbound-api:latest

#    Option B: Load from archive (for offline servers)
#    gzip -dc northbound-api.tar.gz | docker load

# 3. Edit the environment file for your database
vi .env.mysql               # or .env.oracle for Oracle
                             # set MYSQL_HOST, MYSQL_PASSWORD, ACS_URL, etc.

# 4. Make sure compose.yml references the correct env file
#    env_file: .env.mysql    (default) or .env.oracle

# 5. Start the application
docker compose up -d

# 6. Verify
curl -s http://localhost:8080/iot-webservice/actuator/health

To switch between MySQL and Oracle, change the env_file value in compose.yml:

env_file:
  - .env.oracle    # instead of .env.mysql

After startup the application is available at http://localhost:8080.

Preparation

1. Prepare Working Directory

Create the working directory on the host machine:

mkdir -p /usr/local/nbi-api/logs
cd /usr/local/nbi-api

Download the deployment files from the FT_DISK on SharePoint and place them on the server. Alternatively, copy the files from the source repository.

Expected Layout

/usr/local/nbi-api/
+-- .env.mysql                    # Environment variables (MySQL profile)
+-- .env.oracle                   # Environment variables (Oracle profile)
+-- api.properties                # Main API configuration
+-- hazelcast-client.yaml         # Hazelcast cache cluster config
+-- logs/                         # Application logs (mounted volume)

You only need the env file that matches your database (.env.mysql or .env.oracle), not both.

Configuration files are also available in the source repository:

File Path in repository

.env.mysql, .env.oracle

docker/

api.properties

src/main/resources/

hazelcast-client.yaml

src/main/resources/config/

2. Get the Docker Image

# Authenticate with the registry
docker login hub.friendly-tech.com

# Pull the latest image (optional if using docker compose --
# "docker compose up" will pull the image automatically)
docker pull hub.friendly-tech.com/api/northbound-api:latest

To pin to a specific release version:

docker pull hub.friendly-tech.com/api/northbound-api:v1.0.0-b0.0.13

The version v1.0.0-b0.0.13 is an example. Always use the version tag that corresponds to your deployment.

Optionally, retag the image for shorter references in docker run commands:

docker tag hub.friendly-tech.com/api/northbound-api:latest northbound-api:latest

If you skip retagging, use the full image name (hub.friendly-tech.com/api/northbound-api:latest) in all subsequent commands.

To request Harbor access, contact the DevOps team for a user account or robot token.

Option B: Transfer Image to Offline Server

Use this option when the target server has no internet access and cannot pull images from Harbor directly. The image is pulled on a machine that does have Harbor access, exported to a tar archive, transferred to the offline server, and loaded there.

  1. On a machine with Harbor access, log in and pull the image for the target server’s architecture:

    docker login hub.friendly-tech.com
    docker pull --platform linux/amd64 \
      hub.friendly-tech.com/api/northbound-api:<version>

    An explicit --platform matching the offline target server’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. For example, on an Apple Silicon (arm64) Mac without --platform, the resulting archive will be arm64 and will fail with a platform does not match warning on amd64 servers. The example uses linux/amd64; replace it with the platform of your offline target server (linux/arm64, etc.).

  2. Save the pulled image to a tar archive and compress it:

    docker save hub.friendly-tech.com/api/northbound-api:<version> \
      -o northbound-api-<version>.tar
    gzip northbound-api-<version>.tar
  3. Transfer northbound-api-<version>.tar.gz to the offline server (e.g., via scp or removable media).

  4. On the offline server, load the image:

    gzip -dc northbound-api-<version>.tar.gz | docker load

Option C: Build from Source (developers only)

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --secret id=github_token,env=GITHUB_TOKEN \
  --secret id=github_user,src=<(printf '%s' "Friendly-Technologies") \
  -t northbound-api:latest \
  --load .

Building from source requires valid GitHub credentials with access to private dependencies. Loading a pre-built image is faster and avoids build-environment issues.

After loading or pulling, verify the image is available:

docker images | grep northbound-api

3. Configure Environment

The Northbound API reads its configuration from:

  • .env.mysql or .env.oracle — environment variables loaded via Docker --env-file

  • api.properties — application-level properties (mounted to /etc/app/api.properties)

  • hazelcast-client.yaml — cache configuration (mounted to /etc/app/hazelcast-client.yaml)

Choose the appropriate environment file:

MySQL
cd /usr/local/nbi-api
vi .env.mysql
Oracle
cd /usr/local/nbi-api
vi .env.oracle

For comprehensive details on every configuration option, see the Configuration Guide.

If the ACS, database, or Hazelcast cluster runs on the host machine (not in Docker), use one of the following as the hostname in ACS_URL, MYSQL_HOST / ORACLE_HOST, and in hazelcast-client.yaml:

  • host.docker.internal — works on Docker Desktop (macOS, Windows) and on Linux with --add-host=host.docker.internal:host-gateway (for docker run). The provided compose.yml already includes extra_hosts for this.

  • The host machine’s real IP address (e.g., 192.168.1.10)

localhost or 127.0.0.1 will not work — inside the container these point to the container itself, not the host.

Pre-configured template files are available on FT_DISK and in the source repository.

Copy the file that matches your database to the working directory:

cp .env.mysql /usr/local/nbi-api/.env.mysql
# or
cp .env.oracle /usr/local/nbi-api/.env.oracle

TLS Keystore (HTTPS)

To enable HTTPS, provide a PKCS12 keystore file at /etc/app/keystore.p12 inside the container. With the recommended mount (-v $(pwd):/etc/app), place the file in your working directory as keystore.p12.

For local development, you can generate a self-signed keystore:

cd /usr/local/nbi-api

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"

Then set these values in .env.mysql or .env.oracle: SERVER_SSL_ENABLED=true, HTTPS_PORT=8443, SERVER_SSL_KEY_STORE=file:/etc/app/keystore.p12, SERVER_SSL_KEY_STORE_PASSWORD=<keystore-password>, SERVER_SSL_KEY_PASSWORD=<key-password>.

These settings enable HTTPS for the Northbound API container itself. ACS connection settings are configured separately via ACS_URL and ACS_PORT: use http://…​; + 8080 for HTTP ACS, or https://…​; + 443/8443 for HTTPS ACS.

For production, use a certificate issued by your CA/security team and export it to PKCS12 format (.p12).

Deployment

Create a compose.yml file in your working directory:

services:
  northbound-api:
    image: hub.friendly-tech.com/api/northbound-api:latest
    container_name: northbound-api
    env_file:
      - .env.mysql          # or .env.oracle
    ports:
      - "8080:8080"         # <host-port>:<container-port> -- change the host port
                             # if 8080 is already used by another application (e.g. "9080:8080")
      - "8443:8443"         # Optional: publish this mapping only when HTTPS is enabled
    volumes:
      - .:/etc/app
      - ./logs:/app/logs
    extra_hosts:
      - "host.docker.internal:host-gateway"
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/iot-webservice/actuator/health"]
      interval: 30s
      timeout: 3s
      start_period: 60s
      retries: 3

To change external ports, edit only the left side of each mapping (<host-port>:<container-port>), for example 9080:8080 for HTTP and 9443:8443 for HTTPS.

Publishing both mappings at the same time is valid and does not conflict. Conflicts happen only if host ports overlap (left side) or if HTTP/HTTPS are configured to the same container port. For HTTP-only deployment, remove the 8443:8443 mapping.

# Start the application
docker compose up -d

# View logs
docker compose logs -f northbound-api

# Stop
docker compose down

Option 2: docker run

cd /usr/local/nbi-api

docker run -d \
  --name northbound-api \
  --env-file .env.mysql \
  -v $(pwd):/etc/app \
  -v $(pwd)/logs:/app/logs \
  -p 8080:8080 \
  -p 8443:8443 \
  --restart unless-stopped \
  hub.friendly-tech.com/api/northbound-api:latest

If default host ports are busy, change only the left side: -p 9080:8080 -p 9443:8443. For HTTP-only deployment, omit -p 8443:8443.

Replace --env-file .env.mysql with --env-file .env.oracle for Oracle. If you retagged the image locally, use northbound-api:latest instead of the full Harbor path.

Docker Networking

If the Northbound API container must communicate with other containers (database, Hazelcast, etc.), create a shared Docker network:

docker network create northbound-net

Add --network northbound-net to your docker run command or add the network section to your compose.yml. Use container names (not raw IPs) in .env.mysql / .env.oracle to leverage Docker DNS resolution.

To deploy Northbound API alongside Service API and Provision Portal as a single stack, see the Java API Stack Deployment Guide.

Verify Deployment

After starting the container, run the following checks:

# 1. Check container status
docker ps -f name=northbound-api
# Expected: container with status "Up" and port 0.0.0.0:8080->8080/tcp

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

# 2b. HTTPS health check (when SERVER_SSL_ENABLED=true)
curl -ks https://localhost:8443/iot-webservice/actuator/health
# Expected: {"status":"UP"}

# 3. Check application logs
docker logs northbound-api --tail 50
# Look for: "Started NorthboundApiApplication in XX.XXX seconds"

# 4. Swagger UI (open in browser)
# http://localhost:8080/iot-webservice/swagger-ui/index.html
# https://localhost:8443/iot-webservice/swagger-ui/index.html

# 5. Test SOAP endpoint
curl -s http://localhost:8080/iot-webservice/FTACSWS/ACSWS?wsdl | head -5
# Expected: beginning of the WSDL XML document

Environment Variables

Common Configuration

MySQL
# ==========================================================
# Northbound API - Environment Configuration (MySQL)
# ==========================================================

# Spring profile: mysql or oracle
SPRING_PROFILES_ACTIVE=mysql

# Container connector ports
# PORT is the HTTP connector port inside the container.
PORT=8080

# HTTPS/TLS settings
# 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 (set it explicitly, e.g. 8443).
# In dual mode, keep PORT different from HTTPS_PORT (example: PORT=8080, HTTPS_PORT=8443).
# If these container ports are equal, only one connector can bind to that port.
# HTTPS_PORT=8443
SERVER_SSL_ENABLED=false
SERVER_SSL_KEY_STORE=file:/etc/app/keystore.p12
SERVER_SSL_KEY_STORE_PASSWORD=<keystore-password>     # <-- replace when HTTPS enabled
SERVER_SSL_KEY_STORE_TYPE=PKCS12
SERVER_SSL_KEY_PASSWORD=<key-password>                # <-- replace when HTTPS enabled
SERVER_SSL_KEY_ALIAS=server
SERVER_SSL_TRUST_STORE=
SERVER_SSL_TRUST_STORE_PASSWORD=

# Container timezone (default: UTC)
TZ=UTC

# ==========================================================
# Database
# ==========================================================
DB_MAX_POOL_SIZE=10
DB_MIN_IDLE=5
DB_CONNECTION_TIMEOUT_MS=30000

# MySQL connection
MYSQL_HOST=<your-mysql-host>            # <-- replace
MYSQL_PORT=3306
MYSQL_SCHEMA=ftacs
MYSQL_USER=ftacs
MYSQL_PASSWORD=<your-db-password>       # <-- replace
MYSQL_DRIVER_CLASS_NAME=com.mysql.cj.jdbc.Driver
# OneIoT (iotw) schema — second datasource required by the Group Update API.
# Same host/port/user/password as above; only the schema differs.
MYSQL_SCHEMA_IOTW=iotw

# ==========================================================
# ACS Service
# ==========================================================
# Base URL of FTACS ACS service (scheme + host, without port/path).
# Examples:
# - same Docker network service: http://ftacs
# - local host: http://127.0.0.1
# - Docker host gateway: http://host.docker.internal
# - remote host: http://acs.example.internal
# If ACS is exposed via HTTPS, use `https://` (for example: https://acs.example.internal).
# Note: host.docker.internal works on Docker Desktop.
# On Linux, add this to the target service in compose.yml:
# extra_hosts:
#   - "host.docker.internal:host-gateway"
ACS_URL=http://<your-acs-host>          # <-- replace
ACS_PORT=8080                            # 8080 (HTTP) or 443/8443 (HTTPS)
ACS_USERNAME=<acs-user>                 # <-- replace
ACS_PASSWORD=<acs-password>             # <-- replace

# ==========================================================
# SQL Login Queries
# ==========================================================
# Choose ONE set depending on which portal stack is deployed.
#
# Old C# stack (CSR, CPEAdmin):
#   admin.login table — columns: name, password, location_id
#
# New Java/Spring stack (Support Portal, Management Portal):
#   iotw.iotw_user table — columns: username, password, domain_id

# -- Old C# stack (CSR / CPEAdmin) — 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)"

# -- New Java/Spring stack (Support / Management portals) — iotw.iotw_user table --
# SQL_LOGIN_BY_NAME_AND_LOCATION0="SELECT password, domain_id FROM iotw.iotw_user WHERE username = :name AND domain_id = 0"
# SQL_LOGIN_BY_NAME_AND_LOCATIONS="SELECT password, domain_id FROM iotw.iotw_user WHERE username = :name AND domain_id IN (:ids)"
# SQL_LOGIN_BY_NAME_NULL_OR_0="SELECT password, domain_id FROM iotw.iotw_user WHERE username = :name AND (domain_id IS NULL OR domain_id = 0)"

# ==========================================================
# API / Cache
# ==========================================================
API_CONFIG_PATH=file:/etc/app/api.properties
CACHE_CONFIG_PATH=file:/etc/app/

# ==========================================================
# Authentication (JWT)
# ==========================================================
# Secret key used to sign and verify JWT tokens (HMAC-SHA256).
# Requirements:
#   - Minimum 32 characters (256 bits)
#   - Use a random alphanumeric string
#   - Must be unique per environment (dev / staging / prod)
# The default value "friendly" 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

# ==========================================================
# 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
Oracle
# ==========================================================
# Northbound API - Environment Configuration (Oracle)
# ==========================================================

# Spring profile: mysql or oracle
SPRING_PROFILES_ACTIVE=oracle

# Container connector ports
# PORT is the HTTP connector port inside the container.
PORT=8080

# HTTPS/TLS settings
# 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 (set it explicitly, e.g. 8443).
# In dual mode, keep PORT different from HTTPS_PORT (example: PORT=8080, HTTPS_PORT=8443).
# If these container ports are equal, only one connector can bind to that port.
# HTTPS_PORT=8443
SERVER_SSL_ENABLED=false
SERVER_SSL_KEY_STORE=file:/etc/app/keystore.p12
SERVER_SSL_KEY_STORE_PASSWORD=<keystore-password>     # <-- replace when HTTPS enabled
SERVER_SSL_KEY_STORE_TYPE=PKCS12
SERVER_SSL_KEY_PASSWORD=<key-password>                # <-- replace when HTTPS enabled
SERVER_SSL_KEY_ALIAS=server
SERVER_SSL_TRUST_STORE=
SERVER_SSL_TRUST_STORE_PASSWORD=

# Container timezone (default: UTC)
TZ=UTC

# ==========================================================
# Database
# ==========================================================
DB_MAX_POOL_SIZE=10
DB_MIN_IDLE=5
DB_CONNECTION_TIMEOUT_MS=30000

# Oracle connection
ORACLE_HOST=<your-oracle-host>          # <-- replace
ORACLE_PORT=1521
ORACLE_SERVICE=XEPDB1
ORACLE_USER=ftacs                       # shared fallback for both schemas
ORACLE_PASSWORD=<your-db-password>      # <-- replace (shared fallback)
ORACLE_DRIVER_CLASS_NAME=oracle.jdbc.OracleDriver
# Per-schema accounts. FTACS = ACS schema; IOTW = OneIoT schema read by the
# Group Update API (in Oracle the schema is identified by the user account).
# Each falls back to ORACLE_USER / ORACLE_PASSWORD when left unset.
ORACLE_USER_FTACS=ftacs
ORACLE_PASSWORD_FTACS=<your-db-password>    # <-- replace
ORACLE_USER_IOTW=iotw
ORACLE_PASSWORD_IOTW=<your-iotw-password>   # <-- replace

# ==========================================================
# ACS Service
# ==========================================================
# Base URL of FTACS ACS service (scheme + host, without port/path).
# Examples:
# - same Docker network service: http://ftacs
# - local host: http://127.0.0.1
# - Docker host gateway: http://host.docker.internal
# - remote host: http://acs.example.internal
# If ACS is exposed via HTTPS, use `https://` (for example: https://acs.example.internal).
# Note: host.docker.internal works on Docker Desktop.
# On Linux, add this to the target service in compose.yml:
# extra_hosts:
#   - "host.docker.internal:host-gateway"
ACS_URL=http://<your-acs-host>          # <-- replace
ACS_PORT=8080                            # 8080 (HTTP) or 443/8443 (HTTPS)
ACS_USERNAME=<acs-user>                 # <-- replace
ACS_PASSWORD=<acs-password>             # <-- replace

# ==========================================================
# SQL Login Queries
# ==========================================================
# Choose ONE set depending on which portal stack is deployed.
#
# Old C# stack (CSR, CPEAdmin):
#   admin.login table — columns: name, password, location_id
#
# New Java/Spring stack (Support Portal, Management Portal):
#   iotw.iotw_user table — columns: username, password, domain_id

# -- Old C# stack (CSR / CPEAdmin) — 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)"

# -- New Java/Spring stack (Support / Management portals) — iotw.iotw_user table --
# SQL_LOGIN_BY_NAME_AND_LOCATION0="SELECT password, domain_id FROM iotw.iotw_user WHERE username = :name AND domain_id = 0"
# SQL_LOGIN_BY_NAME_AND_LOCATIONS="SELECT password, domain_id FROM iotw.iotw_user WHERE username = :name AND domain_id IN (:ids)"
# SQL_LOGIN_BY_NAME_NULL_OR_0="SELECT password, domain_id FROM iotw.iotw_user WHERE username = :name AND (domain_id IS NULL OR domain_id = 0)"

# ==========================================================
# API / Cache
# ==========================================================
API_CONFIG_PATH=file:/etc/app/api.properties
CACHE_CONFIG_PATH=file:/etc/app/

# ==========================================================
# Authentication (JWT)
# ==========================================================
# Secret key used to sign and verify JWT tokens (HMAC-SHA256).
# Requirements:
#   - Minimum 32 characters (256 bits)
#   - Use a random alphanumeric string
#   - Must be unique per environment (dev / staging / prod)
# The default value "friendly" 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

# ==========================================================
# 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

Application Settings

Variable Description Default Required

SPRING_PROFILES_ACTIVE

Spring profile (mysql or oracle)

-

Yes

PORT

Application HTTP port inside the container

8080

Yes

TZ

Container timezone

UTC

No

API_CONFIG_PATH

Path to api.properties inside the container

file:/etc/app/api.properties

Yes

CACHE_CONFIG_PATH

Path to cache configuration directory inside the container

file:/etc/app/

Yes

Database Configuration

MySQL
Variable Description Default Required

MYSQL_HOST

MySQL server address

<your-mysql-host>

Yes

MYSQL_PORT

MySQL port

3306

Yes

MYSQL_SCHEMA

MySQL schema name

ftacs

Yes

MYSQL_USER

MySQL username

ftacs

Yes

MYSQL_PASSWORD

MySQL password

-

Yes

MYSQL_DRIVER_CLASS_NAME

JDBC driver class name

com.mysql.cj.jdbc.Driver

Yes

MYSQL_SCHEMA_IOTW

OneIoT schema for the iotw datasource (Group Update API). Same host/port/user/password as the ACS datasource.

iotw

No

DB_MAX_POOL_SIZE

Maximum database connection pool size

10

No

DB_MIN_IDLE

Minimum idle connections in pool

5

No

DB_CONNECTION_TIMEOUT_MS

Connection timeout in milliseconds

30000

No

Oracle
Variable Description Default Required

ORACLE_HOST

Oracle server address

<your-oracle-host>

Yes

ORACLE_PORT

Oracle port

1521

Yes

ORACLE_SERVICE

Oracle service name

XEPDB1

Yes

ORACLE_USER

Shared fallback user for both schemas (used when the *_FTACS / *_IOTW overrides are unset)

ftacs

Yes

ORACLE_PASSWORD

Shared fallback password for both schemas

-

Yes

ORACLE_DRIVER_CLASS_NAME

JDBC driver class name

oracle.jdbc.OracleDriver

Yes

ORACLE_USER_FTACS

User owning the ACS (FTACS) schema. Falls back to ORACLE_USER.

ftacs

No

ORACLE_PASSWORD_FTACS

Password for ORACLE_USER_FTACS. Falls back to ORACLE_PASSWORD.

-

No

ORACLE_USER_IOTW

User owning the OneIoT schema read by the iotw datasource (Group Update API). Falls back to ORACLE_USER.

iotw

No

ORACLE_PASSWORD_IOTW

Password for ORACLE_USER_IOTW. Falls back to ORACLE_PASSWORD.

-

No

DB_MAX_POOL_SIZE

Maximum database connection pool size

10

No

DB_MIN_IDLE

Minimum idle connections in pool

5

No

DB_CONNECTION_TIMEOUT_MS

Connection timeout in milliseconds

30000

No

ACS Service Configuration

Variable Description Default Required

ACS_URL

ACS service base URL (scheme + host, without port/path). Use http://…​; for HTTP ACS or https://…​; for HTTPS ACS. Examples: http://ftacs, http://127.0.0.1, http://host.docker.internal, http://acs.example.internal, https://acs.example.internal

http://<your-acs-host>;

Yes

ACS_PORT

ACS service port. Typical values: 8080 (HTTP), 443 or 8443 (HTTPS)

8080

Yes

ACS_USERNAME

ACS authentication username

-

Yes

ACS_PASSWORD

ACS authentication password

-

Yes

Authentication (JWT)

Variable Description Default Required

JWT_TOKEN_SECRET

Secret key for signing JWT tokens (HMAC-SHA256). Minimum 32 characters. Generate with: openssl rand -base64 48

friendly (dev only)

Yes

JWT_TOKEN_EXPIRED

Token expiration time in milliseconds

36000000 (10 hours)

No

The default JWT_TOKEN_SECRET value friendly-tech-jwt-secret-key-2025 is for development only. Never use it in production. Generate a secure value: openssl rand -base64 48.

SQL Login Queries

Queries authenticate portal users by username and domain/location scope. Choose one set depending on which portal stack is deployed:

  • Old C# stack (CSR, CPEAdmin) — uses admin.login table with columns name, password, location_id.

  • New Java/Spring stack (Support Portal, Management Portal) — uses iotw.iotw_user table with columns username, password, domain_id.

Variable Description Default Required

SQL_LOGIN_BY_NAME_AND_LOCATION0

Authenticate user with global access (location/domain = 0)

See .env file

Yes

SQL_LOGIN_BY_NAME_AND_LOCATIONS

Authenticate a user when the location / domain matches one of the allowed IDs

See .env file

Yes

SQL_LOGIN_BY_NAME_NULL_OR_0

Authenticate user when location/domain is unset (NULL) or 0 — treats both as global access

See .env file

Yes

Container Management

Command Description

docker compose up -d

Start all services in the background

docker compose down

Stop and remove containers

docker compose logs -f northbound-api

Follow application logs

docker compose ps

Show container status

docker compose restart northbound-api

Restart the application

docker compose pull

Update images to latest versions

docker exec -it northbound-api sh

Enter the application container

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

Check mounted configuration files

docker logs --tail 100 northbound-api

View last 100 log lines

docker stats northbound-api --no-stream

View resource usage

docker inspect northbound-api --format '{{json .Config.Env}}'

Check environment variables

Updating

docker compose pull downloads the latest image from the registry, then docker compose up -d recreates the container with the new version. Configuration files and logs are preserved.

# 1. Backup configuration
cd /usr/local/nbi-api
tar -czf nbi-backup-$(date +%Y%m%d).tar.gz .env.* api.properties hazelcast-client.yaml

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

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

# 4. Verify
curl -s http://localhost:8080/iot-webservice/actuator/health

Via docker load (offline servers)

When the upgrade target server has no access to the Harbor registry, the new image must be pulled on a machine that does have Harbor access, exported to a tar archive, transferred to the offline server, and loaded there.

  1. On a machine with Harbor access, log in and pull the new image for the target server’s architecture:

    docker login hub.friendly-tech.com
    docker pull --platform linux/amd64 \
      hub.friendly-tech.com/api/northbound-api:<new-version>

    An explicit --platform matching the offline target server’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. For example, on an Apple Silicon (arm64) Mac without --platform, the resulting archive will be arm64 and will fail with a platform does not match warning on amd64 servers. The example uses linux/amd64; replace it with the platform of your offline target server (linux/arm64, etc.).

  2. Save the pulled image to a tar archive and compress it:

    docker save hub.friendly-tech.com/api/northbound-api:<new-version> \
      -o northbound-api-<new-version>.tar
    gzip northbound-api-<new-version>.tar
  3. Transfer northbound-api-<new-version>.tar.gz to the offline server (e.g., via scp or removable media).

  4. On the offline server, load the new image and replace the running container:

    # 1. Load the new image from archive
    gzip -dc northbound-api-<new-version>.tar.gz | docker load
    
    # 2. Replace the container
    docker stop northbound-api
    docker rm northbound-api
    
    docker run -d \
      --name northbound-api \
      --env-file .env.mysql \
      -v $(pwd):/etc/app \
      -v $(pwd)/logs:/app/logs \
      -p 8080:8080 \
      --restart unless-stopped \
      hub.friendly-tech.com/api/northbound-api:<new-version>
    
    # 3. Verify
    curl -s http://localhost:8080/iot-webservice/actuator/health

Rollback

docker stop northbound-api
docker rm northbound-api

# Re-run with the previous image tag
docker run -d --name northbound-api \
  --env-file .env.mysql \
  -v $(pwd):/etc/app \
  -v $(pwd)/logs:/app/logs \
  -p 8080:8080 \
  --restart unless-stopped \
  hub.friendly-tech.com/api/northbound-api:<previous-version-tag>

Production Checklist

# Item Notes

1

Restart policy

restart: unless-stopped is already configured in the examples

2

Health check

Configured for /iot-webservice/actuator/health with a 30s interval

3

Database credentials

Replace default values in the .env file (MYSQL_PASSWORD / ORACLE_PASSWORD)

4

ACS credentials

Replace ACS_USERNAME and ACS_PASSWORD in the .env file

5

JWT secret

Replace default friendly with a secure value: openssl rand -base64 48

6

File permissions

Restrict env files: chmod 600 .env.*

7

Log management

Mount /app/logs volume; configure external log rotation (e.g., logrotate)

8

Timezone

Set TZ in .env (defaults to UTC)

9

Resource limits

Add --memory=2g or deploy.resources.limits in compose.yml for production

Troubleshooting

Start with quick diagnostics:

docker logs northbound-api --tail 100
docker inspect northbound-api --format '{{json .Config.Env}}'
docker exec northbound-api ls -la /etc/app

Common Issues

Could not resolve placeholder 'API_CONFIG_PATH'

Symptom: PlaceholderResolutionException: Could not resolve placeholder 'API_CONFIG_PATH'

Check:

docker exec northbound-api env | grep API_CONFIG

Solution:

  • Verify --env-file points to the correct .env.mysql or .env.oracle file. Use an absolute path if needed: --env-file /usr/local/nbi-api/.env.mysql.

  • Confirm the env file contains API_CONFIG_PATH=file:/etc/app/api.properties.

open .env.mysql: no such file or directory

Symptom: Docker fails to start with a "file not found" error.

Check:

ls -la /usr/local/nbi-api/.env.*

Solution:

  • Run docker run from the directory containing the env file, or use an absolute path: --env-file /usr/local/nbi-api/.env.mysql.

  • Check file permissions: chmod 640 .env.*.

FileNotFoundException: /etc/app/api.properties

Symptom: Application logs show the properties file is missing.

Check:

docker exec northbound-api ls /etc/app
ls -la /usr/local/nbi-api/api.properties

Solution:

  • Confirm api.properties exists in /usr/local/nbi-api/.

  • Verify the volume mount: -v $(pwd):/etc/app.

Database Connection Failures

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

Check:

docker exec northbound-api nc -zv <db-host> <db-port>

Solution:

  • Verify the database host is reachable from the container.

  • Check MYSQL_HOST / ORACLE_HOST and credentials in your .env.* file.

  • If using Docker networking, ensure both containers are on the same network:

    docker network inspect northbound-net

Hazelcast Connection Failure

Symptom: Unable to connect to any address in logs.

Check:

docker exec northbound-api env | grep CACHE_CONFIG
docker exec northbound-api cat /etc/app/hazelcast-client.yaml

Solution:

  • Verify Hazelcast members are running and on the same Docker network.

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

  • Confirm CACHE_CONFIG_PATH in the env file points to the mounted directory.

Port Conflict

Symptom: address already in use error.

Check:

lsof -i :8080
# or
docker ps

Solution:

  • Stop the conflicting service, or expose a different host port: -p 9080:8080.

Build Fails: Permission denied: ./gradlew

Symptom: Docker build stage fails on ./gradlew.

Check:

ls -la gradlew

Solution:

  • On the host: chmod +x gradlew.

  • Ensure your source checkout preserves execute permissions.

Build Fails: Secret Errors

Symptom: Could not read script '/run/secrets/github_user' during build.

Check:

echo $GITHUB_TOKEN | head -c 5

Solution:

  • Provide the required --secret flags when running docker build (see Preparation step 2, Option C).

  • Verify GitHub credentials are valid and have access to private dependencies.

Port Reference

Port Protocol Description

8080

HTTP

HTTP connector (REST, SOAP, Actuator, Swagger UI)

8443

HTTPS

TLS connector port (used when SERVER_SSL_ENABLED=true)

When HTTPS is enabled (SERVER_SSL_ENABLED=true and keystore settings are provided), Tomcat can run with two connectors: HTTPS on HTTPS_PORT (typically 8443) and HTTP on PORT (typically 8080). Use different values for those container ports to keep both connectors active. Publishing both Docker mappings (-p 8080:8080 and -p 8443:8443) is safe.

Maintenance

Backup Configuration

cd /usr/local/nbi-api
tar -czf nbi-backup-$(date +%Y%m%d).tar.gz .env.* api.properties hazelcast-client.yaml

Restart Services

# Restart via compose
docker compose restart northbound-api

# Full restart (recreate container)
docker compose down
docker compose up -d

Stop Services

# Stop (keeps data)
docker compose down

# Stop and remove all data
docker compose down -v