Installation & Deployment

This guide covers deploying the Service API — a Spring Boot 4 application built with Java 25 — as a single Docker container.

The infrastructure the Service API depends on — the ACS database and Hazelcast cluster, plus the Northbound API it calls for device operations — is assumed to be already deployed and reachable; this guide only points the Service API at it.
To deploy that infrastructure, see All in one server deployment or Separate server deployment — Server E: API Stack.
For every configuration item and its default, see Service API Configuration Guide.

1. Overview

The Service API is a single container (service-api) exposing REST and SOAP endpoints under the /iot-webservice context path for device service management and provisioning operations. It is a plain Docker Compose service, built and shipped only as a container image — the Gradle build produces a bootJar (service-api.jar) that goes into the image, and there is no distribution archive for a manual or bare-metal install.

Service API  ->  Northbound API  +  ACS Database (ftacs schema)  +  Hazelcast

Everything to the right of the Service API box in that diagram is a prerequisite, not part of this installation.

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

2. Prerequisites

2.1. Host Requirements

Component Minimum Recommended Notes

Docker Engine

20.10

20.10 or later

If Docker is not installed, follow the Docker Installation Guide

Docker Compose

2.0

2.0 or later

The docker compose plugin, not the legacy docker-compose binary

RAM

2 GB

4 GB

Single JVM container

Free disk

1 GB

1 GB plus room for logs

Image plus rolling logs in logs/ (daily rotation, gzip, 30-day retention)

2.2. Required External Dependencies

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

Component Minimum Version Why It Is Needed Port Required

MySQL or Oracle (ACS database)

verify with FT DevOps — no database version is pinned in this repo; the JDBC drivers used are com.mysql:mysql-connector-j (version managed by Spring Boot 4.0.3) and com.oracle.database.jdbc:ojdbc11:23.26.1.0.0 in build.gradle

Holds the ftacs schema the Service API reads device and service data from. The Service API runs no schema migrations — without the schema created and populated by FTACS setup, every data operation fails.

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

Yes

Hazelcast

5.6.0 (client library com.hazelcast:hazelcast:5.6.0 in build.gradle; server version verify with the Hazelcast cluster owner)

The application context wires a Hazelcast client bean; without a reachable cluster every cache-backed operation fails.

5701 (TCP)

Yes

Northbound API

verify with the Northbound API owner

The Service API calls it over REST to dispatch device operations (GetParameterValues, SetParameterValues, and so on); without it no device operation reaches a device.

8080 (HTTP)

Yes

The Hazelcast dependency is a functional dependency, not an optional cache. The bundled hazelcast-client.yaml uses connection-strategy.async-start: true / reconnect-mode: ASYNC, so the client does not block startup: the container starts and connects in the background. Until a cluster is reachable, cache-backed operations fail, so provide a reachable cluster before serving traffic. (Whether the hazelcastCacheFactory bean itself fails fast depends on its implementation in the external api-common library — verify against a real startup log if strict fail-fast behavior matters.)

Deploying that infrastructure is out of scope here — see All in one server deployment for a single-host stack, or Separate server deployment — Server E for a split topology that brings up Northbound API and Service API together.

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 the FT_DISK — Service-API folder for the deployment files (compose.yml, the shared .env and the per-service service-api/.env) and the pre-configured service-api configuration package (service-api.yml, hazelcast-client.yaml).

3. Network Requirements

Connections the Service API opens to its infrastructure. Each row 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

ACS Database

3306 (Oracle: 1521)

TCP

MYSQL_HOST/MYSQL_PORT or ORACLE_HOST/ORACLE_PORT — device and service data

Hazelcast

5701

TCP

Hazelcast client connection — see cluster-members in hazelcast-client.yaml

Northbound API

8080

TCP

REST calls to dispatch device operations (GetParameterValues, SetParameterValues, and so on) — NORTHBOUND_API_URL

Inbound traffic (the published HTTP/HTTPS port, including the Prometheus scrape of /iot-webservice/actuator/prometheus) is listed in Port Reference.

For a quick connectivity check from the Service API host:

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

3.1. Docker Networking

When the database, Hazelcast cluster, or Northbound API runs on the host machine rather than in Docker, use one of the following as the hostname in MYSQL_HOST / ORACLE_HOST, NORTHBOUND_API_URL, 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: - "host.docker.internal:host-gateway".

  • The host machine’s real IP address (for example 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.

When a peer runs as a container on the same Compose network, use its container name instead (for example http://northbound-api:8080/iot-webservice).

4. Registry Authentication

The Service API image is pulled from hub.friendly-tech.com. Authenticate once per host before the first docker compose up.

docker login hub.friendly-tech.com

Enter the read-only pull credentials when prompted:

Field Value

Username

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 Service API host cannot reach hub.friendly-tech.com, pull the image on a machine that does have registry access, export it to an archive, transfer it, and load it on the offline host.

The connected machine can run Linux, macOS, or Windows — commands are given for both shells below. It does not need to be the same platform as the offline host.

An explicit --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/service-api:$TAG"
    docker save "hub.friendly-tech.com/api/service-api:$TAG" | gzip > "service-api-$TAG.tar.gz"

    Windows (PowerShell):

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

  4. Transfer the archive to the offline host, together with compose.yml, the shared .env, service-api/.env, and the service-api/config/ directory (service-api.yml, hazelcast-client.yaml, and keystore.p12 if HTTPS is enabled).

  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 "service-api-$TAG.tar.gz" | docker load
    docker compose up -d

    Windows (PowerShell):

    $TAG = "latest"
    
    docker load -i "service-api-$TAG.tar"
    docker compose up -d

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/service-api

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

This covers the Service API image only. The ACS database, Hazelcast, and the Northbound API 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 directory the Service API needs on the host and point it at the infrastructure listed in Required External Dependencies.

5.1. Directory Structure

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

The application configuration files live in the config/ subdirectory, which the container mounts read-only at /etc/app. compose.yml and both .env files stay on disk but are never mounted — they are loaded via env_file.

5.1.1. Directory Layout

The Service API slice of the platform deployment layout under /usr/local/ft-system:

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

compose.yml

Docker Compose file for the stack

Yes

.env

Shared stack environment — database connection, Hazelcast, inter-service URLs

Yes

service-api/.env

Per-service environment — host ports, HikariCP pool, TLS, timeout, log level

Yes

service-api/config/

Configuration mounted read-only at /etc/app

Yes

service-api/logs/

Application logs written by the container at /app/logs

No

Files inside service-api/config/:

File Purpose

service-api.yml

Main service configuration — email, speed tests, subscription parameters, service groups, wireless configuration. See Service API Configuration Guide.

hazelcast-client.yaml

Hazelcast cluster client configuration

keystore.p12

PKCS12 TLS keystore — only when HTTPS is enabled, see TLS Keystore (optional)

Download service-api.yml and hazelcast-client.yaml from the FT_DISK — Service-API folder, or copy the source-repository defaults:

File Path in repository

compose.yml

docker/

service-api.yml

src/main/resources/yml/

hazelcast-client.yaml

src/main/resources/config/

5.2. Environment Configuration

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

File Purpose

/usr/local/ft-system/.env

Shared by every service: database connection (DB_PROFILE, SPRING_PROFILES_ACTIVE, DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_SERVICE), Flowable DB, ClickHouse, PostgreSQL, Hazelcast, JWT, inter-service URLs (NORTHBOUND_API_URL), TZ, COMPOSE_PROFILES

/usr/local/ft-system/service-api/.env

Service API only: host ports (SERVICE_API_PORT, SERVICE_API_HTTPS_PORT), container connector ports (PORT, HTTPS_PORT), HikariCP pool tuning (DB_MAX_POOL_SIZE, DB_MIN_IDLE, DB_CONNECTION_TIMEOUT_MS), TLS keystore settings, TIMEOUT, LOGGING_LEVEL_COM_FRIENDLY_APISERVICE, configuration paths (CONFIG_PATH, CACHE_CONFIG_PATH)

Edit both layers:

cd /usr/local/ft-system
vi .env                     # shared: database connection, Hazelcast, NORTHBOUND_API_URL
vi service-api/.env         # per-service: ports, pool, TLS, timeout, log level

MySQL is the default. Switching to Oracle is not a file swap: change DB_PROFILE, SPRING_PROFILES_ACTIVE, DB_HOST, DB_PORT and the other variables listed in Switching to Oracle inside the single root .env.

compose.yml maps the generic DB_* names from the root .env to the vendor-specific names each application expects (MYSQL_HOST, ORACLE_HOST, …​) through the x-db-env YAML anchor, so the Service API — which reads MYSQL_* / ORACLE_* — works unchanged.

Host names for peers that run outside Docker are covered in Docker Networking.

Every variable the container reads, with the default that applies when the variable is not set. The table below lists them by the name the application reads. The MYSQL_* / ORACLE_* / DB_HOST names are supplied by the x-db-env mapping in compose.yml from the root .env — you set DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_SERVICE, DB_PROFILE and SPRING_PROFILES_ACTIVE there, not the vendor-specific names. Everything else in the table (PORT, HTTPS_PORT, the SERVER_SSL_* group, the HikariCP group, TIMEOUT, LOGGING_LEVEL_COM_FRIENDLY_APISERVICE, CONFIG_PATH, CACHE_CONFIG_PATH) belongs in service-api/.env; TZ and NORTHBOUND_API_URL are shared and stay in the root .env. Defaults come from src/main/resources/application.yml, application-mysql.yml, application-oracle.yml, logback-spring.xml and config/HttpsConfiguration.java in this repository; Required means the service cannot serve traffic correctly on a real deployment without it.

Variable Description Default Required

SPRING_PROFILES_ACTIVE

Database profile to activate: mysql or oracle. Selects which application-<profile>.yml datasource is used.

none (the shared .env sets mysql)

Yes

PORT

HTTP connector port inside the container (server.port).

8080

No

SERVER_SSL_ENABLED

Enables the HTTPS connector.

false

No

HTTPS_PORT

HTTPS connector port inside the container. Must differ from PORT for both connectors to bind.

8443

No

SERVER_SSL_KEY_STORE

Keystore location, for example file:/etc/app/keystore.p12.

empty

Yes when SERVER_SSL_ENABLED=true

SERVER_SSL_KEY_STORE_PASSWORD

Keystore password.

empty

Yes when SERVER_SSL_ENABLED=true

SERVER_SSL_KEY_STORE_TYPE

Keystore format.

PKCS12

No

SERVER_SSL_KEY_PASSWORD

Private key password.

empty

Yes when SERVER_SSL_ENABLED=true

SERVER_SSL_KEY_ALIAS

Alias of the key entry used for TLS.

server

No

SERVER_SSL_TRUST_STORE

Truststore location, when client certificates must be validated.

empty

No

SERVER_SSL_TRUST_STORE_PASSWORD

Truststore password.

empty

No

TZ

Container timezone; affects timestamps in logs and in the database.

UTC (set by the image; the shared .env sample overrides it with Europe/Kyiv)

No

DB_MAX_POOL_SIZE

HikariCP maximum pool size.

10

No

DB_MIN_IDLE

HikariCP minimum idle connections.

5

No

DB_CONNECTION_TIMEOUT_MS

HikariCP connection timeout in milliseconds.

30000

No

DB_HOST

Fallback database host, used when MYSQL_HOST / ORACLE_HOST is not set.

localhost

No

MYSQL_HOST

MySQL host, used to build the JDBC URL on the mysql profile.

${DB_HOST}, otherwise localhost

Yes on the mysql profile

MYSQL_PORT

MySQL port.

3306

No

MYSQL_SCHEMA

MySQL schema. Must already exist; it is created by the FTACS setup.

ftacs

No

MYSQL_USER

MySQL user.

ftacs

Yes on the mysql profile

MYSQL_PASSWORD

MySQL password.

ftacs

Yes on the mysql profile

MYSQL_DRIVER_CLASS_NAME

JDBC driver class for MySQL.

com.mysql.cj.jdbc.Driver

No

MYSQL_JDBC_URL

Full JDBC URL. Overrides the URL assembled from MYSQL_HOST / MYSQL_PORT / MYSQL_SCHEMA.

assembled from the variables above

No

ORACLE_HOST

Oracle host, used to build the JDBC URL on the oracle profile.

${DB_HOST}, otherwise localhost

Yes on the oracle profile

ORACLE_PORT

Oracle listener port.

1521

No

ORACLE_SERVICE

Oracle service name.

XEPDB1

No

ORACLE_USER

Oracle user.

ftacs

Yes on the oracle profile

ORACLE_PASSWORD

Oracle password.

ftacs

Yes on the oracle profile

ORACLE_DRIVER_CLASS_NAME

JDBC driver class for Oracle.

oracle.jdbc.OracleDriver

No

ORACLE_JDBC_URL

Full JDBC URL. Overrides the URL assembled from ORACLE_HOST / ORACLE_PORT / ORACLE_SERVICE.

assembled from the variables above

No

NORTHBOUND_API_URL

Base URL of the Northbound API, including the /iot-webservice context path.

http://localhost:8080/iot-webservice

Yes

CONFIG_PATH

Directory the service-api.yml configuration files are read from. In Docker this points into the mounted configuration directory (file:/etc/app/).

classpath:yml

Yes

CACHE_CONFIG_PATH

Directory the hazelcast-client.yaml file is read from.

classpath:config/

Yes

TIMEOUT

Default timeout for device operations, in seconds.

60

No

LOGGING_LEVEL_COM_FRIENDLY_APISERVICE

Log level for com.friendly.apiservice. Values: ERROR, WARN, INFO, DEBUG. In DEBUG business errors include stack traces.

DEBUG (logback-spring.xml)

No

The two environment files below set the same variables; the table is the reference, the files are the starting point to edit.

Click to expand .env (shared stack environment — Service API relevant subset)
# ==========================================================
# Shared stack environment - /usr/local/ft-system/.env
# Loaded by every service via env_file.
# ==========================================================

# Container timezone; affects timestamps in logs and in the database
TZ=Europe/Kyiv

# ==========================================================
# Main Database Configuration
# Generic names. compose.yml maps them to the vendor-specific
# names the Service API reads (MYSQL_HOST / ORACLE_HOST, ...)
# via the x-db-env anchor. MySQL is the default.
# ==========================================================
DB_PROFILE=mysql
SPRING_PROFILES_ACTIVE=mysql
DB_HOST=mysql                           # <-- replace when the DB is outside Docker
DB_PORT=3306
DB_USER=ftacs
DB_PASSWORD=<your-db-password>          # <-- replace
DB_SERVICE=XEPDB1                       # Oracle PDB service name; unused for MySQL

# ==========================================================
# Inter-Service URLs
# If both services are in the same Docker network, use the container name.
# If Northbound API runs on a separate host, use http://<host>:<port>/iot-webservice
# ==========================================================
NORTHBOUND_API_URL=http://northbound-api:8080/iot-webservice
Click to expand service-api/.env (per-service environment)
# ==========================================================
# Service API - per-service environment
# /usr/local/ft-system/service-api/.env
# ==========================================================

# Host ports published on the server. Map to the container ports below.
SERVICE_API_PORT=8085
SERVICE_API_HTTPS_PORT=9445

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

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

# ==========================================================
# Configuration Paths (inside container)
# ==========================================================
CONFIG_PATH=file:/etc/app/
CACHE_CONFIG_PATH=file:/etc/app/

# ==========================================================
# Logging
# ==========================================================
# Log level for com.friendly.apiservice. Values: ERROR, WARN, INFO, DEBUG.
# In DEBUG mode business errors include stack traces. Can also be changed
# at runtime via Actuator.
LOGGING_LEVEL_COM_FRIENDLY_APISERVICE=DEBUG

# ==========================================================
# Timeouts
# ==========================================================
# Default timeout for device operations (seconds)
TIMEOUT=60

hazelcast-client.yaml ships with development defaults (cluster-name: dev, cluster-members: [127.0.0.1:5701]); point it at the real cluster before starting:

hazelcast-client:
  cluster-name: <your-cluster-name>
  network:
    cluster-members:
      - <hazelcast-server-host>:<hazelcast-server-port>

For the full reference of service-api.yml and every environment variable, see Service API Configuration Guide.

5.3. TLS Keystore (optional)

The Service API serves HTTPS from a single PKCS12 keystore, keystore.p12, placed in config/ alongside the other configuration files, so it is mounted at /etc/app/keystore.p12.

For local development, generate a self-signed keystore:

cd /usr/local/ft-system/service-api/config

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"

The keystore must end up in /usr/local/ft-system/service-api/config/keystore.p12 — that directory is mounted read-only at /etc/app, so the file appears as /etc/app/keystore.p12. Confirm it before continuing:

ls -l /usr/local/ft-system/service-api/config/keystore.p12

Then set these values in service-api/.env: 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>.

Finally uncomment the ${SERVICE_API_HTTPS_PORT:-9445}:8443 line in the ports: mapping of compose.yml and recreate the container — the HTTPS port is not published by default.

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

6. Deployment

6.1. Startup Dependencies

The Service API has no depends_on on its infrastructure — the ACS database, Hazelcast, and Northbound API live outside this stack, so Compose cannot gate on their health. All of them should be reachable before the Service API serves traffic. The Hazelcast client connects during context initialisation; with the bundled async-start: true config it connects asynchronously and does not block startup, but operations that use the cache fail until the cluster is up (see Required External Dependencies).

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
until nc -z <hazelcast-host-ip> 5701; do sleep 2; done

# Wait for Northbound API to be healthy
until curl -sf http://<nbi-host-ip>:8080/iot-webservice/actuator/health; do sleep 2; done

# Then start Service API
docker compose up -d

The Service API 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 (Service API)
version: "3.8"

services:
  service-api:
    image: hub.friendly-tech.com/api/service-api:latest
    container_name: service-api
    env_file:
      - .env
      - ./service-api/.env
    ports:
      - "${SERVICE_API_PORT:-8085}:8080"          # HTTP
      # - "${SERVICE_API_HTTPS_PORT:-9445}:8443"  # Optional: publish when HTTPS is enabled
    volumes:
      - ./service-api/config:/etc/app:ro
      - ./service-api/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
    networks:
      - service-api

networks:
  service-api:
    driver: bridge

This follows the source repository’s docker/compose.yml, with three deliberate differences: the configuration mount is narrowed to the config/ layout used above (the repository file mounts the whole service directory), the env_file list is replaced by the two-layer .env + service-api/.env model the platform uses, and the published HTTP port is the platform-assigned 8085, not the 8080 in the repository file — on a /usr/local/ft-system host, 8080 is already published by FTACS. To pin a specific release instead of latest, change the image: tag. The database vendor is not selected here — it is selected by DB_PROFILE / SPRING_PROFILES_ACTIVE / DB_HOST in the root .env, see Switching to Oracle.

The HTTPS mapping in this file is 9445 → 8443 (host 9445 to container 8443), not 8443 → 8443, and it is commented out by default — uncomment it when HTTPS is enabled. Both host ports (8085 HTTP, 9445 HTTPS) are the values the platform assigns to the Service API in the full-system environment reference; do not move them to 8080/8443, which FTACS already occupies on the same host.

cd /usr/local/ft-system
docker login hub.friendly-tech.com
docker compose up -d
docker compose ps
# Follow startup
docker compose logs -f service-api | grep -i "started"
curl -s http://localhost:8085/iot-webservice/actuator/health
Running the container without Compose

The same container can be started directly with docker run:

cd /usr/local/ft-system

docker run -d \
  --name service-api \
  --env-file .env \
  --env-file service-api/.env \
  -v $(pwd)/service-api/config:/etc/app:ro \
  -v $(pwd)/service-api/logs:/app/logs \
  -p 8085:8080 \
  -p 9445:8443 \
  --add-host=host.docker.internal:host-gateway \
  --restart unless-stopped \
  hub.friendly-tech.com/api/service-api:latest

The port mappings are the platform-assigned ones: 8085 → 8080 (HTTP) and 9445 → 8443 (HTTPS). If those host ports are busy, change only the left side: -p 9080:8080 -p 10443:8443. For an HTTP-only deployment, omit -p 9445:8443. Both --env-file flags are required, in this order — the per-service file must come last so its values win.

7. Verification

7.1. Startup Log

Wait 30—​60 seconds after start, then check the logs:

docker compose logs service-api | tail -20
# or, without Compose:
docker logs service-api --tail 20

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

INFO --- [service-api] [main] c.f.apiservice.config.StartupInfoLogger :
============================================================
Service 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/FTServiceAPI
SOAP WSDL        : /iot-webservice/FTACSWS/FTServiceAPI?wsdl
API version      : <version>
API build        : <build>
Java runtime     : <java-version>
PID              : <pid>
============================================================

7.2. Endpoint Checks

# 1. Check container status
docker ps -f name=service-api
# Expected: container with status "Up" and mapped ports

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

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

# 3. Swagger UI (open in browser)
# http://localhost:8085/iot-webservice/swagger-ui/index.html

7.3. Database Connectivity

The Actuator health endpoint runs with show-details: always, so the db component reports the JDBC connection state:

curl -s http://localhost:8085/iot-webservice/actuator/health | grep -o '"db":{[^}]*}'
# Expected: "db":{"status":"UP",...}

If the component is missing or DOWN, check reachability from inside the container and the credentials in the root .env:

docker exec service-api nc -zv <db-host> 3306      # Oracle: 1521
docker exec service-api env | grep -E 'MYSQL_|ORACLE_'

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

8085 → 8080

HTTP

REST and SOAP API under /iot-webservice, Swagger UI, Actuator. Container port set by PORT.

Public (published by compose.yml)

9445 → 8443

HTTPS

Same endpoints over TLS when SERVER_SSL_ENABLED=true. Container port set by HTTPS_PORT.

Not published by default — the mapping is commented out in compose.yml

By default the application runs on HTTP only (SERVER_SSL_ENABLED=false, PORT=8080). When HTTPS is enabled it can run both connectors simultaneously as long as PORT and HTTPS_PORT differ.

8.2. Outbound Connections

Ports the Service API dials on the existing infrastructure — confirm them against whoever operates that infrastructure, and set the matching variables in the root .env.

Destination Port Protocol Purpose

ACS database

3306 (Oracle: 1521)

TCP

MYSQL_HOST/MYSQL_PORT or ORACLE_HOST/ORACLE_PORT

Hazelcast cluster

5701

TCP

cluster-members in hazelcast-client.yaml, path set via CACHE_CONFIG_PATH

Northbound API

8080

HTTP

NORTHBOUND_API_URL

8.3. HTTP Endpoints

Method Path Purpose Auth

GET

/iot-webservice/actuator/health

Health check, also 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 documentation interface

None

GET

/iot-webservice/soap-api.yaml

SOAP API definition, listed in the Swagger UI configuration

None

POST

/iot-webservice/FTACSWS/FTServiceAPI

SOAP endpoint; the WSDL is served at the same path with ?wsdl

None

The application ships without Spring Security — no dependency on the classpath and no filter chain in the source — and management.endpoints.web.exposure.include is *. Restrict access to the published port at the network level.

9. Stack Management

9.1. Logs

docker compose logs -f service-api      # follow application logs
docker logs --tail 100 service-api      # last 100 lines without Compose

The application writes rolling logs to logs/application.log on the host (daily rotation, gzip-compressed, 30-day retention — see logback-spring.xml).

9.2. Start, Stop, Restart

Command Description

docker compose up -d

Start the service in the background

docker compose down

Stop and remove the container

docker compose restart service-api

Restart the application

docker compose ps

Show container status

9.3. Shell Access

Command Description

docker exec -it service-api sh

Enter the application container

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

Check mounted configuration files

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

Check environment variables

9.4. Updating Service API

# 1. Backup configuration
cd /usr/local/ft-system
tar -czf service-api-backup-$(date +%Y%m%d).tar.gz .env service-api/.env service-api/config/

# 2. Pull the latest image and recreate the container
docker compose pull service-api
docker compose up -d service-api

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

For servers without Harbor access, use the manual image transfer flow in Offline Servers instead of docker compose pull.

10. Production Checklist

  • Restart policy: restart: unless-stopped is set in compose.yml.

  • Health check: /iot-webservice/actuator/health configured with a 30s interval.

  • Database credentials replaced in the root .env (DB_USER / DB_PASSWORD).

  • NORTHBOUND_API_URL points to the correct Northbound API instance and includes the /iot-webservice path.

  • cluster-name and cluster-members in hazelcast-client.yaml match the real cluster.

  • TLS: the self-signed keystore.p12 replaced with a CA-issued certificate if HTTPS is enabled.

  • File permissions restricted on env files: chmod 600 .env service-api/.env.

  • Docker log rotation configured: --log-opt max-size=50m --log-opt max-file=5.

  • TZ set in the root .env (the sample uses Europe/Kyiv).

  • Resource limits set (--memory=2g or deploy.resources.limits in compose.yml).

  • Access to the published port restricted at the network level — the application has no built-in authentication.

11. Troubleshooting

Start with quick diagnostics:

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

11.1. Could not resolve placeholder 'CONFIG_PATH'

Symptom: PlaceholderResolutionException: Could not resolve placeholder 'CONFIG_PATH'

Fix:

  1. Verify env_file in compose.yml lists both .env and ./service-api/.env.

  2. Confirm service-api/.env contains CONFIG_PATH=file:/etc/app/.

  3. Validate inside the container: docker exec service-api env | grep CONFIG_PATH.

11.2. Hazelcast Unreachable — Cache Operations Fail

Symptom: The container starts, but the log shows repeated Hazelcast client reconnect attempts or Unable to connect to any address in the config, and cache-backed operations fail.

Cause: The Hazelcast client cannot reach the cluster. The bundled hazelcast-client.yaml uses async-start: true / reconnect-mode: ASYNC, so the container does not exit — it starts and keeps retrying the connection in the background — but any operation that relies on the cache fails until a cluster is reachable.

Fix:

  1. Verify Hazelcast is running and reachable: docker exec service-api nc -zv <hazelcast-host> 5701.

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

  3. Confirm CACHE_CONFIG_PATH in service-api/.env points to the mounted directory containing hazelcast-client.yaml.

11.3. FileNotFoundException: /etc/app/service-api.yml

Symptom: Application logs show the configuration file is missing.

Fix:

  1. Confirm service-api.yml exists in the config/ directory: ls -la /usr/local/ft-system/service-api/config/service-api.yml.

  2. Verify the volume mount, from /usr/local/ft-system: -v $(pwd)/service-api/config:/etc/app:ro (or ./service-api/config:/etc/app:ro in compose.yml).

  3. Check inside the container: docker exec service-api ls /etc/app.

11.4. Logs Are Not Written to the Host Directory

Symptom: application.log exists in the container, but logs/ on the host is empty or missing.

Fix:

  1. Add a bind mount for logs, from /usr/local/ft-system: -v $(pwd)/service-api/logs:/app/logs (already present in compose.yml as ./service-api/logs:/app/logs).

  2. Create the host directory before starting: mkdir -p /usr/local/ft-system/service-api/logs.

  3. Verify mount configuration: docker inspect service-api --format '{{json .Mounts}}'.

11.5. Database Connection Failure

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

Fix:

  1. Verify the database host is reachable from the container:

    docker exec service-api nc -zv <db-host> <db-port>
  2. Check DB_HOST, DB_PORT, DB_USER and DB_PASSWORD in the root .env — compose.yml maps them to MYSQL_HOST / ORACLE_HOST and the matching credentials.

  3. Confirm the ftacs schema exists — the Service API does not create it.

11.6. Northbound API Connection Failure

Symptom: Connection refused or timeout errors when calling Northbound API.

Fix:

  1. Verify NORTHBOUND_API_URL in the root .env is correct and includes the /iot-webservice path.

  2. Check that the Northbound API is running: curl -s http://<nbi-host>:8080/iot-webservice/actuator/health.

  3. If using Docker networking, ensure both containers are on the same network and use the container name instead of localhost.

11.7. Port Already in Use

Symptom: address already in use error.

Fix:

  1. Find the process using the port:

    lsof -i :8085
    # or
    docker ps
  2. Stop the conflicting service, or publish a different host port: -p 9080:8080.

11.8. Build Fails: Permission denied: ./gradlew

Symptom: Building the image from source (docker buildx build) fails on ./gradlew.

Fix:

  1. On the host: chmod +x gradlew.

  2. Ensure your source checkout preserves execute permissions.

11.9. Build Fails: Secret Errors

Symptom: Could not read script '/run/secrets/github_user' during a from-source build.

Fix:

  1. Dockerfile requires GitHub Packages credentials passed as BuildKit secrets: --secret id=github_user,…​ --secret id=github_token,…​.

  2. Verify the credentials are valid and have access to the private dependencies the build pulls.

11.10. Getting Support

When raising an issue with Friendly Technologies support, attach:

  • docker logs service-api --tail 500 and the relevant part of logs/application.log.

  • docker inspect service-api --format '{{json .Config.Env}}' with passwords removed.

  • The image tag in use: docker images hub.friendly-tech.com/api/service-api.

  • Which database vendor and profile is active (SPRING_PROFILES_ACTIVE).