Runbook: Install Exporters

Version 1.6.9-option-3 | Updated: August 07, 2026

Runbook: Install Exporters

2. Install Node Exporter and Process Exporter

Target servers: acs1, acs2, hc1, hc2, db, *-api servers

Use the automated deployment script with service auto-detection:

# Auto-detect services and deploy (recommended):
cd /opt/grafana
sudo bash /opt/grafana/misc/quick-deploy.sh PROMETHEUS_IP

# Examples:
sudo bash /opt/grafana/misc/quick-deploy.sh 65.108.67.167

# Or deploy specific exporters manually (--exporter flag):
sudo bash /opt/grafana/misc/quick-deploy.sh 65.108.67.167 --exporter base --exporter jmx-wildfly
sudo bash /opt/grafana/misc/quick-deploy.sh 65.108.67.167 --exporter base --exporter mysql

# Or use one-command bootstrap (installs Docker, clones repo, deploys):
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- 65.108.67.167

--exporter flag: Specifies which exporters to deploy from exporters/ folder. Use base (always required) plus service-specific: jmx-wildfly, mysql, oracle, jmx-hazelcast, clickhouse, postgres, nginx. Hazelcast: embedded-in-ACS needs no exporter (native :9101), standalone/docker needs jmx-hazelcast (JMX :9110 → :9101). The jmx-wildfly directory holds two services: the HikariCP exporter (5556) starts always, while the JVM exporter (5557) sits behind the jvm compose profile. Docker FTACS has a built-in javaagent on 5557, so quick-deploy enables that profile only on bare-metal installations.

Service Auto-Detection: The script automatically detects running services and deploys appropriate exporters:

Detected Service Exporter Directory Detection Method

WildFly/FTACS

jmx-wildfly (JVM exporter via profile jvm)

jboss-modules.jar, -Djboss.home.dir

Hazelcast

(native prometheus)

HazelcastMemberStarter, hazelcast-*.jar

MySQL/MariaDB

mysql

mysqld process, systemd service

Oracle DB

oracle

ora_pmon_* process

ClickHouse

clickhouse

clickhouse-server process, systemd service

PostgreSQL

postgres

postgres process, systemd service

Nginx (Angular UI)

nginx

Docker container portals, nginx process on port 8880

FT Device Network

(native endpoint)

Docker container ft-device-network

AI Agent

(native endpoint)

Docker container ui-ai-agent

UI Backend

(native endpoint)

Docker container ui-backend

Provision API

(native endpoint)

Docker container provision-api

Northbound API

(native endpoint)

Docker container northbound-api

Service API

(native endpoint)

Docker container service-api

(always)

base

Always deployed (node + process + cadvisor)

The script automatically:

  • ✅ Detects running services on the server

  • ✅ Shows deployment summary with confirmation prompt

  • ✅ Prompts for database credentials if needed (MySQL/Oracle)

  • ✅ Deploys Node Exporter (port 9100) - system metrics

  • ✅ Deploys Process Exporter (port 9256) - per-process metrics

  • ✅ Deploys service-specific exporters based on detection

  • ✅ Configures firewall rules with persistence

  • ✅ Verifies deployment

Manual Installation (click to expand)

If you prefer manual installation or need to customize the setup:

2.0 Optional: Change Exporter Ports

All exporters are configured to run in network_mode: host (they bind directly to host network). By default, they use these ports:

Exporter Default Port Purpose

Node Exporter

9100

System metrics

Process Exporter

9256

Host process metrics

cAdvisor

9183

Docker container metrics

MySQL Exporter

9104

MySQL database metrics

Oracle DB Exporter

9161

Oracle database metrics

ClickHouse (native)

9363

ClickHouse database metrics

PostgreSQL Exporter

9187

PostgreSQL database metrics

Nginx Exporter

9113

Nginx web server metrics

JMX Exporter

5556

JMX metrics (Hikari)

JMX Exporter

5557

JVM metrics

If you need to override these ports (e.g., due to conflicts or multi-instance deployment):

  • Uncomment the relevant --web.listen-address=…​ or command: […​] line in docker-compose.yml.

  • Update firewall and Prometheus scrape configs accordingly.

2.1 Get the Repository

We have two options for obtaining the necessary files: download the repository or clone it.

Option A: Download an archive

Take it from the hub, which carries an archive of every branch, rebuilt on each push to it:

cd /opt
curl -u ast -fsSLo grafana-1.6.9.tar.gz https://hub.friendly-tech.com/repo/1.6.9.tar.gz
tar xzf grafana-1.6.9.tar.gz          # extracts as grafana-1.6.9/
sudo mv grafana-1.6.9 grafana         # the layout below expects /opt/grafana

It asks for a password: the archives are a private repository’s source, so the directory is behind basic auth. The ast account is the read-only one already used to pull our images.

Take the version branch, never the repository’s default — main only receives release merges and lacks the current fixes. A tarball rather than the ZIP: unpacking needs only tar, while unzip is absent from a minimal Debian or RHEL install. The archive holds one top-level directory, so it has to be renamed — unpacking it into /opt/grafana would give /opt/grafana/grafana-1.6.9/… and nothing would find its configuration there.

Option B: Clone with Git

Run the following commands in the shell console:

cd /opt
git clone https://github.com/Friendly-Technologies/grafana.git

Enter your username and password if prompted (make sure you have access to this repo).

2.2 File Structure

After downloading or cloning, you should have the following file structure:

/opt
└── grafana
    ├── exporters/                    # Modular exporter directories
    │   ├── base/                     # Node + Process + cAdvisor (all servers)
    │   │   ├── docker-compose.yml
    │   │   ├── process-exporter.yml
    │   │   └── .env.example
    │   ├── jmx-wildfly/             # HikariCP exporter (5556) + JVM exporter
    │   │                            #   (5557, compose profile 'jvm', bare-metal only)
    │   │   ├── docker-compose.yml
    │   │   ├── config-hikari.yml
    │   │   ├── config-jvm.yml
    │   │   └── .env.example
    │   ├── mysql/                    # MySQL/MariaDB exporter
    │   │   ├── docker-compose.yml
    │   │   └── .env.example
    │   ├── oracle/                   # Oracle DB exporter
    │   │   ├── docker-compose.yml
    │   │   ├── config.yaml.template
    │   │   ├── generate-config.sh
    │   │   └── .env.example
    │   ├── clickhouse/               # ClickHouse native Prometheus endpoint (no exporter container)
    │   │   ├── clickhouse-prometheus.xml   # config.d snippet (port 9363)
    │   │   └── install-native.sh           # installs the config + optional restart
    │   ├── postgres/                 # PostgreSQL exporter
    │   │   ├── docker-compose.yml
    │   │   └── .env.example
    │   └── nginx/                    # Nginx exporter (for Angular UI)
    │       ├── docker-compose.yml
    │       └── .env.example
    ├── prometheus-grafana-stack/     # Monitoring stack
    │   ├── docker-compose.yml
    │   ├── .env.example
    │   └── prometheus/
    │       ├── prometheus.yml
    │       ├── scripts/
    │       │   └── generate-targets.sh
    │       └── targets/
    │           └── servers.env.example
    ├── docs/
    │   └── architecture.md           # Architecture diagrams
    ├── misc/
    │   ├── bootstrap.sh              # One-command installer
    │   ├── quick-deploy.sh           # Auto-detect and deploy exporters
    │   ├── detect-services.sh        # Service auto-detection
    │   ├── diagnose.sh               # Troubleshooting tool
    │   └── install-docker.sh         # Docker installer
    ├── README.md
    └── sizing.md                     # Capacity planning guide

2.3 Deploy Exporters

The deployment automatically detects running services and deploys appropriate exporters:

# Auto-detect services and deploy (recommended)
cd /opt/grafana
sudo bash /opt/grafana/misc/quick-deploy.sh 65.109.58.165

For manual deployment, navigate to the relevant exporter directory:

# Base exporters (Node + Process) - all servers
cd /opt/grafana/exporters/base

# Hazelcast: No exporter needed - uses native Prometheus endpoint (port 9101)
# See section "Prerequisites: Enable Prometheus on Hazelcast Servers"

# JMX exporters for WildFly/FTACS
cd /opt/grafana/exporters/jmx-wildfly
  1. Configure environment variables: Copy the example environment file and edit it with your configuration:

cp .env.example .env
vi .env

The .env file contains configuration for exporter versions, ports, and other settings. Most values have sensible defaults and can be left as-is. Key variables:

  • NODE_EXPORTER_PORT - Default: 9100

  • PROCESS_EXPORTER_PORT - Default: 9256

  • JMX_HIKARI_PORT - Default: 5556 (for ACS servers)

  • JMX_JVM_PORT - Default: 5557 (for ACS servers) Note: If you don’t create a .env file, the default values will be used automatically.

    1. Start the exporters using Docker Compose:

docker compose up -d

Hint: You can check the logs of the started containers:

docker compose logs -f
  1. Enable Prometheus to access exporter ports:

iptables -A INPUT -p tcp -s $PROMETHEUS_IP --dport 9100 -j ACCEPT   # node-exporter
iptables -A INPUT -p tcp -s $PROMETHEUS_IP --dport 9256 -j ACCEPT   # process-exporter
iptables -A INPUT -p tcp -s $PROMETHEUS_IP --dport 9183 -j ACCEPT   # cadvisor
# JMX ports (if WildFly detected):
iptables -A INPUT -p tcp -s $PROMETHEUS_IP --dport 5556 -j ACCEPT   # jmx-hikari
iptables -A INPUT -p tcp -s $PROMETHEUS_IP --dport 5557 -j ACCEPT   # jmx-jvm
# Hazelcast native Prometheus (if Hazelcast detected):
iptables -A INPUT -p tcp -s $PROMETHEUS_IP --dport 9101 -j ACCEPT   # hazelcast native

In our example we took ip address of prometheus and commands should be looking like that:

iptables -A INPUT -p tcp -s 65.109.58.165 --dport 9100 -j ACCEPT   # node-exporter
iptables -A INPUT -p tcp -s 65.109.58.165 --dport 9256 -j ACCEPT   # process-exporter
iptables -A INPUT -p tcp -s 65.109.58.165 --dport 9183 -j ACCEPT   # cadvisor
# JMX ports (if WildFly detected):
iptables -A INPUT -p tcp -s 65.109.58.165 --dport 5556 -j ACCEPT   # jmx-hikari
iptables -A INPUT -p tcp -s 65.109.58.165 --dport 5557 -j ACCEPT   # jmx-jvm
# Hazelcast native Prometheus (if Hazelcast detected):
iptables -A INPUT -p tcp -s 65.109.58.165 --dport 9101 -j ACCEPT   # hazelcast native

3. Install JMX Exporter (WildFly/ACS Servers)

Target servers:

  • acs1, acs2 - WildFly/FTACS application servers

Note: Hazelcast monitoring depends on how Hazelcast runs:

  • Embedded in ACS/WildFly — the native Prometheus endpoint on :9101 already emits the full com_hazelcast_* MBean set (incl. isClusterSafe); no exporter needed.

  • Standalone / docker Hazelcast — the native endpoint (PROMETHEUS_PORT=9101) emits only com_hazelcast_Metrics_* (no isClusterSafe), so the dashboard shows “No data”. Deploy the jmx-hazelcast exporter instead — it scrapes Hazelcast JMX (:9110) and serves :9101 with the full set. Auto-detection deploys it automatically when it finds a standalone Hazelcast with JMX enabled.

Skip this section entirely on ACS 6.5 and newer. From 6.5 the platform serves JVM and HikariCP metrics through its own Actuator endpoint, which the ACS_METRICS target already scrapes on :8080 — measured on 6.5.2 and 7.1.1, 98 metric families including jvm_memory_used_bytes, jvm_gc_* and hikaricp_connections_*. Deploying jmx-wildfly there adds nothing: the exporter connects to a JMX port the container does not open and serves six metrics about itself. ACS 6.4 and earlier serve a legacy endpoint with 14 business metrics and no JVM data at all, so those hosts do need the JMX exporters described below.

Check which one you have: curl -s http://<acs>:8080/actuator/prometheus | head -1 answers with # HELP on 6.5+, and 404s on 6.4.

Skip this section if: Auto-detection already deployed JMX exporters.

JMX Exporter Types

Server Type Exporter Directory Ports Metrics

WildFly/FTACS (6.4 and earlier)

jmx-wildfly (JVM part: profile jvm)

5556 / 5557

HikariCP pool / JVM heap/GC/threads

FTACS 6.5+

(none — Actuator on :8080)

8080

Same data, via ACS_METRICS

Hazelcast

(native prometheus)

9101

Cluster members, partitions, maps, JVM

Quick Installation

cd /opt/grafana

# Auto-detect (recommended) - deploys appropriate exporters based on detected services
sudo bash /opt/grafana/misc/quick-deploy.sh 65.108.67.167

# Or deploy specific exporters manually:
# For WildFly/FTACS servers:
sudo bash /opt/grafana/misc/quick-deploy.sh 65.108.67.167 --exporter base --exporter jmx-wildfly

Note: The --exporter flag specifies which exporters to deploy from exporters/ folder.

  • base - always required (node-exporter + process-exporter + cAdvisor)

  • jmx-wildfly, mysql, oracle - service-specific exporters

  • Hazelcast: embedded-in-ACS uses the native Prometheus endpoint (:9101); standalone/docker needs the jmx-hazelcast exporter (JMX :9110 → :9101)

Verify JMX Exporters

# Check containers are running
docker ps | grep jmx

# Test metrics endpoints
# WildFly:
curl -s http://localhost:5556/metrics | head  # HikariCP
curl -s http://localhost:5557/metrics | head  # JVM

# Hazelcast:
curl -s http://localhost:9101/metrics | head  # Hazelcast

Important: JMX ports are service-specific — do NOT mix them:

  • FTACS/WildFly JMX: port 9999 (configured in standalone.conf)

  • Hazelcast JMX: port 9110 (configured via HZ_JMX_INTERNAL_PORT env var)

  • Hazelcast Prometheus: port 9101 — embedded-in-ACS: native endpoint (full MBean set). Standalone/docker: served by the jmx-hazelcast exporter scraping JMX :9110 (native alone omits isClusterSafe).

JMX Exporters (ports 5556/5557) connect to FTACS JMX on port 9999. For standalone Hazelcast, the jmx-hazelcast exporter connects to Hazelcast JMX on :9110 and serves Prometheus on :9101. Embedded-in-ACS Hazelcast uses its native :9101 endpoint (no JMX connection needed).

Manual: Enable JMX on WildFly/ACS (port 9999)

Before JMX exporters can collect metrics, WildFly/ACS (NOT Hazelcast) must be configured to expose JMX on port 9999. One-time configuration on each ACS server.

Step 1: Add to /usr/local/FTACS/bin/standalone.conf:

JAVA_OPTS="$JAVA_OPTS \
    -Dcom.sun.management.jmxremote \
    -Dcom.sun.management.jmxremote.port=9999 \
    -Dcom.sun.management.jmxremote.rmi.port=9999 \
    -Dcom.sun.management.jmxremote.ssl=false \
    -Dcom.sun.management.jmxremote.authenticate=false \
    -Djava.rmi.server.hostname=127.0.0.1"

Step 2: Enable HikariCP MBeans in /usr/local/FTACS/standalone/configuration/ftacs_hikari.properties:

registerMbeans=true

Step 3: Restart ACS:

# For MySQL:
service jboss restart

# For Oracle:
service jboss restartoracle

Tip: Check current DB type: ps aux | grep -o '\-Dftacs.usedDBMS=[^ ]*'

Verify: ss -tlnp | grep 9999

Manual: Enable Prometheus on Hazelcast

Hazelcast uses native Prometheus endpoint on port 9101. The quick-deploy.sh script can configure this automatically for native installations.

Native installation (systemd/init.d):

Step 1: Add to /usr/local/hazelcast-5.0/bin/hz-start:

export PROMETHEUS_PORT=9101

Step 2: Restart Hazelcast: systemctl restart hazelcast5

Hazelcast in Docker:

If Hazelcast runs in a Docker container, add the following to your docker run script:

  -e PROMETHEUS_PORT=9101 \   # Enable native Prometheus metrics
  -p 9101:9101 \              # Expose metrics port to host

Then restart the container.

Note: No separate JMX exporter is needed for Hazelcast — it exposes metrics natively on port 9101. JMX exporters (ports 5556/5557) are only for WildFly/FTACS.

Verify: curl -s http://localhost:9101/metrics | head


4. Install MySQL Exporter

Target server: db

Use the automated deployment script:

# If you already have Docker installed and repository cloned:
cd /opt/grafana
sudo bash /opt/grafana/misc/quick-deploy.sh 65.109.58.165 --exporter base --exporter mysql

# Or use one-command bootstrap (auto-detects MySQL):
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- 65.109.58.165

Important: Before running the script, create the MySQL exporter user (see step 1 below).

Manual Installation (click to expand)

Run the following commands in the shell console on the target server:

4.0 Optional: Change Exporter Port

By default, MySQL Exporter listens on port 9104. To override it (e.g., due to a conflict or multiple exporters):

  1. Open /opt/grafana/exporters/mysql/docker-compose.yml

  2. Uncomment and modify the --web.listen-address=:PORT parameter in the command: block:

# Uncomment to override default port (9104)
# - '--web.listen-address=:19104'

4.1 Setup Steps

  1. Create an exporter user in MySQL: Log in as root or admin and run the following commands (generate a secure password and replace StR0nG_PassW0rD_hErE accordingly):

CREATE USER IF NOT EXISTS 'exporter'@'%' IDENTIFIED BY 'StR0nG_PassW0rD_hErE' WITH MAX_USER_CONNECTIONS 3;
GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'exporter'@'%';
  1. Clone or download this repo into /opt/grafana, in the same way as described in step 2.

  2. Configure environment variables: Navigate to the MySQL exporter directory and create the .env file:

cd /opt/grafana/exporters/mysql/
cp .env.example .env
vi .env

Update the following variables with your MySQL credentials:

# MySQL Connection Details (REQUIRED)
MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_USER=exporter
MYSQL_PASSWORD=StR0nG_PassW0rD_hErE  # Replace with your password

Optional variables (can be left as default):

  • MYSQLD_EXPORTER_VERSION - Default: latest

  • MYSQLD_EXPORTER_PORT - Default: 9104

    1. Start the exporter using Docker Compose:

docker compose up -d
  1. Enable Prometheus to access exporter ports:

iptables -A INPUT -p tcp -s $PROMETHEUS_IP --dport 9104 -j ACCEPT

5. Install Oracle DB Exporter

Target server: oracle-db or any server with Oracle Database access

Use the automated deployment script:

# If you already have Docker installed and repository cloned:
cd /opt/grafana
sudo bash /opt/grafana/misc/quick-deploy.sh 65.109.58.165 --exporter base --exporter oracle

# Or use one-command bootstrap (auto-detects Oracle):
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- 65.109.58.165

Note: Bootstrap and quick-deploy will prompt for Oracle credentials interactively.

Manual Installation (click to expand)

Run the following commands in the shell console on the target server:

5.1 Setup Oracle DB Exporter

  1. Clone or download this repo into /opt/grafana, in the same way as described in step 2.

  2. Navigate to the Oracle exporter directory:

cd /opt/grafana/exporters/oracle/
  1. Configure environment variables: Create the .env file with your Oracle Database credentials:

cp .env.example .env
vi .env

Update the following variables:

# Oracle Database Connection (REQUIRED)
ORACLE_HOST=localhost
ORACLE_PORT=1521
ORACLE_SERVICE_NAME=ORCL
ORACLE_USER=system
ORACLE_PASSWORD=your_oracle_password

Optional variables (can be left as default):

  • ORACLE_EXPORTER_VERSION - Default: 2.2.0

  • ORACLE_EXPORTER_PORT - Default: 9161

    1. Generate config.yaml from template:

./generate-config.sh

This script reads .env and generates config.yaml with substituted values. . Start the exporter using Docker Compose:

docker compose up -d

Hint: You can check the logs of the started container:

docker compose logs -f
  1. Verify the exporter is working:

curl http://localhost:9161/metrics

You should see Oracle Database metrics including:

  • oracledb_sessions_value - Database sessions

  • oracledb_tablespace_bytes - Tablespace usage

  • oracledb_activity_* - Database activity metrics

  • oracledb_resource_* - Resource utilization

    1. Enable Prometheus to access exporter port:

iptables -A INPUT -p tcp -s $PROMETHEUS_IP --dport 9161 -j ACCEPT

In our example:

iptables -A INPUT -p tcp -s 65.109.58.165 --dport 9161 -j ACCEPT

Remote Database Monitoring

If your database runs on a separate server (e.g., Windows server, cloud RDS, or a different network segment) where you cannot install Docker exporters, you can run the exporter on any Linux server that has network access to the database.

Step 1: On the Linux server (where exporter will run), set the host to the remote database IP:

cd /opt/grafana/exporters/oracle
cp .env.example .env
vi .env
ORACLE_HOST=<database-server-ip>    # Remote database server IP (not localhost)
ORACLE_PORT=1521
ORACLE_SERVICE_NAME=<service-name>
ORACLE_USER=<username>
ORACLE_PASSWORD=<password>

Step 2: Generate config and start:

cd /opt/grafana/exporters/oracle
bash /opt/grafana/exporters/oracle/generate-config.sh
docker compose up -d

Step 3: Verify exporter connects to remote database:

curl http://localhost:9161/metrics | grep oracledb_up
# Expected: oracledb_up 1

Step 4: Add to Prometheus targets. Since the exporter runs locally, use 127.0.0.1:

cd /opt/grafana/prometheus-grafana-stack/prometheus/targets
vi servers.env
# Add line:
ORACLE 127.0.0.1:9161 <instance-name>

# Regenerate targets:
bash /opt/grafana/prometheus-grafana-stack/prometheus/scripts/generate-targets.sh

Note: This approach works for any database exporter (MySQL, PostgreSQL, ClickHouse, Oracle). The exporter connects to the database over the network — only the database port (e.g., 1521, 3306) needs to be open between the exporter and the database server. No Docker or exporter installation is needed on the database server itself.

Firewall: Ensure the database port is open from the exporter server to the database server. No need to open exporter port (9161) externally — Prometheus scrapes it locally.

Mixed Deployment (Some Services on Same Server as Grafana)

When some monitored services run on the same server as Grafana/Prometheus but in a separate Docker Compose stack, those containers may be in a different Docker network. Prometheus runs in network_mode: host and cannot always reach containers in bridge networks through docker-proxy (connections hang/timeout).

Services on other servers are not affected — standard port mapping works fine over the network.

How to identify the problem:

Solution: Switch only the affected service to network_mode: host. Services on other servers keep their normal networking.

Example: Hazelcast on same server as Grafana

Before (bridge network — metrics not accessible from Prometheus):

hazelcast:
    image: hub.friendly-tech.com/components/hazelcast:ft.5.5.0
    ports:
      - "5701:5701"
      - "5711:5711"
      - "9101:9101"
    environment:
      PROMETHEUS_PORT: 9101

After (host network — metrics accessible on localhost):

hazelcast:
    image: hub.friendly-tech.com/components/hazelcast:ft.5.5.0
    network_mode: host
    environment:
      PROMETHEUS_PORT: 9101

network_mode: host puts every one of the service’s ports on all interfaces, including the public one — there is no ports: list left to restrict. For Hazelcast that means 5701 and the JMX port face the internet, and an open Hazelcast member port is a known remote-code-execution vector. Firewall them in the DOCKER-USER chain (UFW alone does not apply to Docker traffic — see docs/ufw-docker-firewall-guide-en.md):

iptables -I DOCKER-USER -p tcp -m multiport --dports 5701,5711,9101,9110 \
         ! -s <monitoring-server-ip>/32 -j DROP

Important changes when switching a service to network_mode: host:

  • Remove ports: section for that service (not needed — all ports are on host directly)

  • Replace container hostname references with 127.0.0.1 in environment variables:

  • HZ_MEMBERS=hazelcast:5701HZ_MEMBERS=127.0.0.1:5701

  • HZ_PUBLIC_ADDRESS=hazelcast:5711HZ_PUBLIC_ADDRESS=127.0.0.1:5711

  • Other containers that connect to this service by Docker DNS name must also be updated to use 127.0.0.1 or the host IP

Prometheus target configuration:

For services on the same server — use 127.0.0.1:

JMX_HC 127.0.0.1:9101 <instance-name>

For services on other servers — use their IP as usual:

JMX_HC <server-ip>:9101 <instance-name>

Verify:

# Should return metrics immediately (no hang)
curl -s http://localhost:9101/metrics | head

Note: Only services co-located with Grafana/Prometheus need this change. Services on separate servers work with standard bridge networking and port mapping.

Note: This issue occurs because Prometheus uses network_mode: host and docker-proxy between host and bridge networks can be unreliable on some kernel/Docker versions.


6. Install FT UI Services Monitoring (DEV-524)

Target server: hc1 (65.109.24.154) - Angular UI services server

This section covers monitoring for the Angular UI services stack deployed via Docker Compose.

Components Monitored

Service Type Port Metrics Path Description

portals (nginx)

Web server

8880/8843

via exporter

Angular UI frontend

ui-backend

Spring Boot

8881

/actuator/prometheus

Angular backend API

ft-device-network

Spring Boot

8383

/api/actuator/prometheus

WiFi mesh service

ui-ai-agent

Python/FastAPI

8084

/metrics

AI assistant

clickhouse

Database

9363

/metrics (native endpoint)

Analytics database

postgres

Database

5432

via exporter

AI agent database

The bootstrap script automatically detects FT UI services when run on HC1:

# Auto-detect and deploy all exporters (recommended):
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- PROMETHEUS_IP

# Or if repository already cloned:
cd /opt/grafana
sudo bash /opt/grafana/misc/quick-deploy.sh PROMETHEUS_IP

The script will automatically deploy:

  • nginx-exporter (port 9113) - for Angular UI (portals container)

  • ClickHouse native Prometheus endpoint (port 9363) - for ClickHouse database

  • postgres-exporter (port 9187) - for PostgreSQL database

Native endpoints (no exporter needed):

  • ft-device-network (port 8383) - Spring Boot Actuator

  • ai-agent (port 8084) - FastAPI with prometheus_client

  • ⚠️ ui-backend (port 8881) - Requires Spring Security configuration (see below)

Manual Installation

Nginx Exporter (for Angular UI portals)

Step 1: Enable nginx stub_status in portals container

The portals nginx must expose /nginx_status endpoint. Add the following block to the nginx config inside the portals container:

location /nginx_status {
    stub_status on;
    access_log off;
    allow 127.0.0.1;
    deny all;
}

How to find and edit the config:

# Check where nginx config is located inside the container
docker exec portals nginx -T 2>/dev/null | head -5

# Common locations:
#   /etc/nginx/conf.d/default.conf
#   /etc/nginx/nginx.conf

# View the current config
docker exec portals cat /etc/nginx/conf.d/default.conf

# Edit: copy config out, add the location block, copy back
docker cp portals:/etc/nginx/conf.d/default.conf ./default.conf
# ... add the location /nginx_status block inside the server { } section ...
docker cp ./default.conf portals:/etc/nginx/conf.d/default.conf
docker exec portals nginx -s reload

Verify stub_status is working:

docker exec portals curl -s http://127.0.0.1/nginx_status
# Expected output:
#   Active connections: 5
#   server accepts handled requests
#
1234 1234 5678
#   Reading: 0 Writing: 1 Waiting: 4

Note: This change needs to be persisted in the portals Docker image or compose volume mount, otherwise it will be lost on container restart.

Step 2: Deploy nginx-exporter

cd /opt/grafana/exporters/nginx
cp .env.example .env
vi .env

Update:

NGINX_HOST=localhost  # or container name if using Docker network
NGINX_PORT=8880
NGINX_EXPORTER_PORT=9113

Step 3: Start exporter

docker compose up -d

Step 4: Configure firewall

iptables -A INPUT -p tcp -s $PROMETHEUS_IP --dport 9113 -j ACCEPT

Verify:

curl http://localhost:9113/metrics | grep nginx_

ClickHouse metrics (native Prometheus endpoint)

ClickHouse exposes Prometheus metrics natively — no exporter container. The old f1yegor/clickhouse-exporter is not used: it crash-loops on modern ClickHouse (21.8+) because it does not sanitise block-device async metric names (clickhouse_block_discard_ops_dm-4 is not a valid Prometheus metric name).

Step 1: Enable the native endpoint on the ClickHouse host (port 9363)

sudo bash /opt/grafana/exporters/clickhouse/install-native.sh --restart

This drops clickhouse-prometheus.xml into /etc/clickhouse-server/config.d/ and restarts clickhouse-server. Omit --restart to install the config and restart the DB yourself later. The endpoint binds to ClickHouse’s configured <listen_host> — if ClickHouse already answers on the server IP (HTTP 8123), 9363 binds there too.

Step 2: Configure firewall

iptables -A INPUT -p tcp -s $PROMETHEUS_IP --dport 9363 -j ACCEPT

Step 3: Add to the Prometheus server’s servers.env, then run generate-targets.sh

CLICKHOUSE  <server-ip>:9363  <instance>  role=database

Verify (native metric names, not clickhouse_*):

curl -s http://localhost:9363/metrics | grep -E '^ClickHouse(Metrics|ProfileEvents|AsyncMetrics)_'

PostgreSQL Exporter

Step 1: Create exporter user in PostgreSQL

-- Use a generated password, not a memorable one; this account is reachable
-- from wherever the database is. e.g.  openssl rand -base64 24
CREATE USER exporter WITH PASSWORD 'REPLACE_WITH_GENERATED_PASSWORD';
GRANT pg_monitor TO exporter;
GRANT CONNECT ON DATABASE ftl_ai TO exporter;

Step 2: Let the exporter reach PostgreSQL — bind to localhost, never to all interfaces

Publishing a database port as "5432:5432" exposes it to the internet. Docker writes its own iptables rules and bypasses UFW, so a firewall that looks correct does not protect a published port. This is not theoretical: this exact line was followed on a customer host and cryptominers appeared in both containers the next day. Full explanation in the repo: docs/ufw-docker-firewall-guide-en.md (RU: -ru.md).

The exporter runs with host networking, so binding to loopback is enough:

postgres:
  ports:
    - "127.0.0.1:5432:5432"   # loopback ONLY — the exporter reaches it, the internet does not

If the database must stay reachable from other hosts (published as 5432:5432 for other services), do not rely on UFW. Restrict it in the DOCKER-USER chain, which Docker does not overwrite:

iptables -I DOCKER-USER -p tcp --dport 5432 ! -s <allowed-ip>/32 -j DROP

Preferred alternative — publish nothing at all. Attach the exporter to the database’s own Docker network; it then talks to PostgreSQL container-to-container and no port is exposed anywhere:

cd /opt/grafana/exporters/postgres
# find the database's network
docker inspect <postgres-container> --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{end}}'
# in .env:
#   POSTGRES_DOCKER_NETWORK=ft-ui-services_default
#   POSTGRES_HOST=postgres
# service name, not 127.0.0.1
#   POSTGRES_PORT=5432
docker compose -f docker-compose.yml -f docker-compose.network.yml up -d

This is also the only variant that works on a hardened host that drops outbound traffic to private ranges: a rule such as OUTPUT -d 172.16.0.0/12 -j DROP covers every default Docker subnet, so a host-networked exporter cannot reach the container even by IP, while a container on the same bridge is unaffected.

Step 3: Deploy postgres-exporter

cd /opt/grafana/exporters/postgres
cp .env.example .env
vi .env

Update:

POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=exporter
POSTGRES_PASSWORD=<the generated password from Step 1>
POSTGRES_DB=ftl_ai
POSTGRES_EXPORTER_PORT=9187

Step 4: Start exporter

docker compose up -d

Step 5: Configure firewall

iptables -A INPUT -p tcp -s $PROMETHEUS_IP --dport 9187 -j ACCEPT

Verify:

curl http://localhost:9187/metrics | grep pg_

Native Metrics Endpoints

These services expose Prometheus metrics natively - no separate exporter needed:

FT Device Network Service (Spring Boot Actuator)

Port: 8383 Path: /api/actuator/prometheus Type: Spring Boot Actuator

Metrics are enabled by default with micrometer-prometheus dependency.

Verify:

curl http://localhost:8383/api/actuator/prometheus | grep jvm_

Add to Prometheus (servers.env):

FT_DEVICE_NETWORK 65.109.24.154:8383 DevOps-HC1

Configure firewall:

iptables -A INPUT -p tcp -s $PROMETHEUS_IP --dport 8383 -j ACCEPT

AI Agent (Python/FastAPI)

Port: 8084 Path: /metrics Type: FastAPI with prometheus_client middleware

Metrics are exposed using prometheus_client library.

Expose the metrics port — the default ft-ui-services compose does not publish the AI Agent port to the host. Add a port mapping to the ui-ai-agent service in the ft-ui-services compose file and recreate the container:

  ui-ai-agent:
    ports:
      - "8084:8080"
docker compose up -d ui-ai-agent

Verify:

curl http://localhost:8084/metrics | grep python_

Add to Prometheus (servers.env):

AI_AGENT 65.109.24.154:8084 DevOps-HC1

Configure firewall:

iptables -A INPUT -p tcp -s $PROMETHEUS_IP --dport 8084 -j ACCEPT

UI Backend (Spring Boot Actuator) - Optional

Port: 8881 Path: /actuator/prometheus Type: Spring Boot Actuator

⚠️ Important: UI Backend requires Spring Security configuration to allow /actuator/** endpoints.

Prerequisites:

Add to Spring Security configuration:

.requestMatchers("/actuator/**").permitAll()

Or update application.properties:

management.endpoints.web.exposure.include=prometheus,health,info

Verify:

curl http://localhost:8881/actuator/prometheus | grep jvm_

Add to Prometheus (servers.env):

UI_BACKEND 65.109.24.154:8881 DevOps-HC1

Configure firewall:

iptables -A INPUT -p tcp -s $PROMETHEUS_IP --dport 8881 -j ACCEPT

Grafana Dashboard

The FT UI Services dashboard (ft-ui-services.json) provides monitoring for all components:

  • Nginx (Portals) - Active connections, requests/sec, response times

  • ClickHouse - Queries, connections, disk usage, replication lag

  • PostgreSQL - Connections, transactions, cache hit ratio, replication

  • FT Device Network - JVM heap, threads, HTTP requests, API latency

  • AI Agent - Python memory, request rate, response times

  • UI Backend - JVM metrics, HTTP requests, database connections (when enabled)

Dashboard is auto-provisioned at: Dashboards → Extended → FT UI Services


7. Install Windows Exporter

Target server: iis

Note: Windows Exporter installation is manual only (no automated script available).

Download and Install

  1. Download from Windows Exporter Releases

  2. Run in Command Prompt (as Administrator):

lodctr.exe /E:Lsa
lodctr.exe /E:PerfProc
lodctr.exe /R

msiexec /i "C:\Users\Administrator\windows_exporter-0.31.3-amd64.msi" ENABLED_COLLECTORS=ad,adfs,cache,cpu,cpu_info,container,dfsr,dhcp,dns,fsrmquota,iis,logical_disk,memory,msmq,mssql,net,os,process,remote_fx,service,tcp,time,vmware LISTEN_PORT=9100 EXTRA_FLAGS="--collector.process.include=""(w3wp|java.*)"" --collector.process.iis --collector.process.counter-version=2"

Configure Firewall

netsh advfirewall firewall add rule name="Allow Windows Exporter" dir=in action=allow protocol=TCP localport=9100

Verify

sc query windows_exporter

Add to Monitoring

After Windows Exporter is running, add the server to servers.env on the Prometheus server:

# === IIS SERVERS ===
NODE <server-ip>:9100 <Prefix>-IIS service=IIS,role=webserver,node_ip=<server-ip>

Then regenerate targets:

cd /opt/grafana/prometheus-grafana-stack/prometheus
/opt/grafana/prometheus-grafana-stack/prometheus/scripts/generate-targets.sh

Note: Windows Exporter uses the NODE type since it exposes metrics on the same port (9100) in a compatible format. The service=IIS label is used by the Windows/IIS Grafana dashboard for filtering.