Monitoring Exporters and Grafana Installation Guide
- Monitoring Exporters and Grafana Installation Guide
- Table of Contents
- Introduction
- Test Environment (Local Stack)
- Quick Start
- 1. Install Docker and Docker Compose on All Linux Servers
- 2. Install Node Exporter and Process Exporter
- 3. Install JMX Exporter (WildFly/ACS Servers)
- 4. Install MySQL Exporter
- 5. Install Oracle DB Exporter
- 6. Install FT UI Services Monitoring (DEV-524)
- 7. Install Windows Exporter
- 8. Configure and Deploy Prometheus with Grafana
- 9. Managing Grafana Dashboards Provisioning
- 10. Alertmanager Configuration
- 11. Downgrade / Rollback
- Capturing a Bootstrap Run
- Verifying the Installation
- Troubleshooting
- Prometheus cannot connect to exporter
- Cannot Login to Grafana
- Spaces in .env Files
- Forgetting to Create .env File
- Firewall Rules Not Persistent
- Wrong Prometheus IP in Firewall
- Conflicting Container Names
- MySQL exporter shows mysql_up 0
- Nginx 502 Bad Gateway
- cAdvisor shows no container metrics (containerd image store)
- Grafana alerts not sending emails
- Alertmanager Config UI: “Failed to update .env file”
- Operations
- For Developers
Version 1.6.9-option-1 | Updated: August 07, 2026
Monitoring Exporters and Grafana Installation Guide
Introduction
This guide provides step-by-step instructions for setting up monitoring exporters and Grafana for observability across various servers. The setup includes Node Exporter, Process Exporter, cAdvisor, MySQL Exporter, Oracle DB Exporter, ClickHouse (native Prometheus endpoint), PostgreSQL Exporter, Nginx Exporter, Windows Exporter, JMX Exporter for WildFly/ACS, and a Prometheus-Grafana-Alertmanager stack deployment. Hazelcast is covered two ways: embedded-in-ACS via its native Prometheus endpoint (:9101, full MBean set), standalone/docker via the bundled jmx-hazelcast exporter (JMX :9110 → :9101), because the native endpoint of a standalone Hazelcast omits isClusterSafe and the Metrics_* MBeans the dashboard needs. FT UI Services (Angular UI backend, AI Agent, Device Network Service, APIs) expose native Spring Boot Actuator or FastAPI metrics endpoints. Alertmanager handles alert notifications via Email, Telegram, Slack, MS Teams, Webhooks, SMS, and SNMP. Following this guide will ensure proper monitoring, alerting, and visualization of system metrics, database performance, application health, and web service performance.
Recent Updates (DEV-524 - FT UI Services Monitoring):
-
✅ Nginx Exporter for Angular UI (portals container)
-
✅ ClickHouse metrics via native Prometheus endpoint (port 9363)
-
✅ PostgreSQL Exporter for AI agent database
-
✅ Native metrics from: ft-device-network, ai-agent, ui-backend
-
✅ Native metrics from APIs: provision-api, northbound-api, service-api
-
✅ New Grafana dashboard: FT UI Services (comprehensive monitoring for all components)
System Requirements
Minimum per exporter node:
-
OS: Ubuntu 20.04+, Debian 11+, CentOS 7+, Rocky Linux 8+
-
RAM: 2GB (4GB recommended)
-
Disk: 10GB free space
-
Network: Outbound internet access (for Docker image downloads)
Prometheus/Grafana server:
-
CPU: 4 vCPU
-
RAM: 16GB
-
Disk: 100GB+ (see
sizing.mdfor capacity planning)
Documentation:
-
Architecture Overview - Diagrams, server types, and port reference
-
Sizing Guide - Capacity planning for Prometheus/Grafana
Test Environment (Local Stack)
To reproduce a complete environment for testing — the FTACS platform and the monitoring on one machine, from a host with nothing installed — follow the Local Stack guide. It covers the whole path end to end: fetching the code, starting the platform, deploying the exporters against it, the monitoring stack, what a healthy result looks like, and how to read the failures that actually occur.
See the Local Stack — Test Environment Guide.
Note that local-stack deploys the platform itself, so it does not belong on a server that already runs FTACS — there, deploy only the exporters as described below.
Quick Start
Setting up a test environment rather than monitoring real servers? Use the Local Stack — Test Environment Guide: it brings up the FTACS platform and the monitoring together on one machine, from scratch.
ONE-COMMAND INSTALL (Recommended):
For monitored servers (exporters):
# Auto-detect services and deploy appropriate exporters:
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- PROMETHEUS_IP
# Example:
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- 65.108.67.167
# With custom instance prefix (default: DevOps):
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- 65.108.67.167 --prefix Prod
Install specific version (dev/testing):
# Install a specific version (required for testing dev branches):
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- --branch 1.6.9 PROMETHEUS_IP
# Example:
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- --branch 1.6.9 65.108.67.167
For Prometheus/Grafana server (monitoring stack):
# Deploy Prometheus + Grafana + Nginx proxy with interactive configuration:
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- stack
# Stack with specific version:
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- stack --branch 1.6.9
Bootstrap options:
| Option | Description | Default | Example |
|---|---|---|---|
|
Instance name prefix for servers.env output |
|
|
|
GitHub token for private repository access |
- |
|
|
Use specific version branch or tag |
|
|
|
Show all available versions (tags + branches) |
- |
|
|
Rollback deployment changes |
latest |
|
|
List available rollback manifests |
- |
|
|
Collect system diagnostics into a report file |
- |
|
|
Reset Grafana admin password |
- |
|
|
Reset Prometheus basic auth password |
- |
|
|
Show bootstrap version |
- |
|
|
Show documentation links |
- |
|
|
Configure exporters from a running local-stack without asking |
- |
|
|
Ignore a running local-stack entirely |
- |
|
|
Alias of |
- |
|
|
Verbose tracing for troubleshooting the installer itself |
- |
|
Examples:
# List all available versions (releases and dev branches)
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | bash -s -- --version-list
# Show documentation links
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | bash -s -- --docs
# Reset Grafana admin password (if forgotten or DB persisted from previous install)
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- --branch 1.6.9 --reset-password "`new_password`"
# Reset Prometheus basic auth password
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- --branch 1.6.9 --reset-prometheus-password "`new_password`"
# Install specific version
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- stack --branch 1.6.4
On the interactive path (no -y), bootstrap.sh stack opens a full-screen
arrow-key configuration menu — a hub with sections for Server (IP
access domain), Prometheus (port, retention, basic-auth), Grafana
(port, admin login, shared SMTP), FTACS Database (MySQL/Oracle for the
Business dashboard), SSL / HTTPS (self-signed / Let’s Encrypt / existing),
Alerting (drill into Email/Telegram/Slack/Teams/Webhook/SMS/SNMP), and
UI Portal integration, ending in Review & apply.
-
↑↓ + Enter to navigate; Esc goes back; each section/field shows a help panel and passwords are masked. Nothing is written until you choose Apply.
-
Add
-yfor a non-interactive run (no menu — auto-derives the domain from the server IP and generates random passwords).
What bootstrap does (exporters):
-
Installs Docker and Docker Compose (if not present)
-
Clones this repository to
/opt/grafana(or updates existing) -
Stashes local changes before update (with restore instructions)
-
Auto-detects running services (Hazelcast, WildFly, MySQL, Oracle, etc.)
-
Shows deployment summary with 20-second countdown
-
Prompts for database credentials interactively (MySQL/Oracle)
-
Deploys appropriate exporters based on detected services
-
Configures firewall rules for Prometheus access
Auto-detected services:
| Service | Exporter Directory | Ports |
|---|---|---|
WildFly/FTACS HikariCP |
|
5556 |
WildFly/FTACS JVM (bare-metal only) |
|
5557 |
Hazelcast |
(native prometheus) |
9101 |
MySQL/MariaDB |
|
9104 |
Oracle DB |
|
9161 |
ClickHouse |
|
9363 |
PostgreSQL |
|
9187 |
Nginx (Angular UI) |
|
9113 |
FT UI Services |
(native endpoints) |
8383, 8084, 8881 |
FT APIs |
(native endpoints) |
8091, 9880, 8085 |
(always) |
|
9100, 9256, 9183 |
Base exporters include:
-
node-exporter(9100) - system metrics -
process-exporter(9256) - host process metrics -
cadvisor(9183) - Docker container metrics
FT UI Services (native Spring Boot Actuator / FastAPI metrics):
-
ft-device-network(8383) - WiFi mesh management service -
ai-agent(8084) - AI assistant (Python/FastAPI) -
ui-backend(8881) - Angular UI backend (when actuator enabled)
FT API Services (native Spring Boot Actuator metrics):
-
provision-api(8091) - Provision Portal API -
northbound-api(9880) - Northbound integration API -
service-api(8085) - Service management API
Note: Bootstrap does not support Windows. For Windows servers, install Windows Exporter manually (see section 7).
Manual/Offline Installation (air-gapped servers)
For servers without internet access. Requires pre-downloaded files.
Prerequisite: Docker and Docker Compose must already be installed on the air-gapped server —
/opt/grafana/misc/install-docker.shneeds internet access (OS package repositories). Use your OS vendor’s offline packages if Docker is missing.
-
On a machine with internet: download the
1.6.9version branch as ZIP —https://github.com/Friendly-Technologies/grafana/archive/refs/heads/1.6.9.zip(extracts asgrafana-1.6.9). ⚠️ Do NOT use the defaultmainbranch —mainonly receives release merges and lacks the current fixes. -
Download all Docker images into a bundle:
bash misc/download-docker-images.sh-
⚠️ Build the bundle on a host whose Docker uses the classic image store. Docker Desktop and Docker Engine ≥ 28 default to the containerd image store, whose
docker savewrites an OCI-format archive that older Docker on the target rejects withunrecognized image formatondocker load. Use an x86_64 Linux host with the classic (overlay2) store, or disable containerd:/etc/docker/daemon.json→{"features":{"containerd-snapshotter":false}}thensystemctl restart dockerand rebuild. -
Verify the archive is classic before shipping (must contain
manifest.json, notoci-layout):tar tf docker-images-bundle.tar* | grep -m1 manifest.json && echo CLASSIC_OK -
Images are pulled for
linux/amd64by default (override:PLATFORM=linux/arm64 bash misc/download-docker-images.sh) -
The bundle also includes the locally-built stack images (
snmp-catcher,alertmanager-config-ui) -
Grafana plugin packages are downloaded into
docker-images-bundle-grafana-plugins/(Grafana installs plugins from grafana.com at container start, which fails air-gapped) -
Preview without downloading:
bash misc/download-docker-images.sh --list-only
-
-
Transfer files to the server:
scp grafana.zip docker-images-bundle.tar.gz user@server:/tmp/ && scp -r docker-images-bundle-grafana-plugins user@server:/tmp/ -
On the target server:
# Load Docker images sudo docker load -i /tmp/docker-images-bundle.tar.gz # Extract repository and deploy exporters cd /opt && sudo unzip /tmp/grafana.zip && sudo mv grafana-* grafana cd grafana && sudo bash /opt/grafana/misc/quick-deploy.sh PROMETHEUS_IP # Auto-detects services
What this does:
-
Loads pre-downloaded Docker images into the local Docker storage
-
Deploys exporters from local images (no internet required)
-
Configures firewall rules for Prometheus access
Monitoring stack node (Prometheus/Grafana) offline: once the image bundle is loaded (
docker load), install the stack — no manual section-8 steps needed. Pick the mode:# Interactive (recommended for a manual install) — runs the configurator and # PROMPTS for GRAFANA_DOMAIN, admin/prometheus passwords, SMTP, etc.: sudo bash /opt/grafana/misc/bootstrap.sh stack # Non-interactive (for curl|bash / headless) — asks NOTHING. It auto-derives # GRAFANA_DOMAIN from the server IP (self-signed cert) and generates random # admin/prometheus passwords, printed in the final summary: sudo bash /opt/grafana/misc/bootstrap.sh stack -yBoth use localhost for Prometheus.
-ynever ships the.envCHANGE_MEplaceholders — the must-have values (IP/domain, passwords, service token) are filled automatically; only optional bits (SMTP, Business-dashboard DB) stay unset.With Docker already present and the repo in place, bootstrap skips the internet-only steps (Docker install, git clone, Let’s Encrypt) and does the rest offline: creates
.envfrom.env.example, generates a self-signed cert + htpasswd + the alertmanager placeholder, starts all containers from the pre-loaded images, deploys the base exporters, and generates targets. It also auto-stages the bundled Grafana plugins: it looks for adocker-images-bundle-grafana-plugins/folder — with the plugin.zipfiles directly inside it, no subfolder — in/opt,/tmpand/opt/grafana, checked in that order, and copies its zips intografana/data/offline-plugins/; the Grafana entrypoint then installs them from disk and skips grafana.com. Auto-staging only runs whenoffline-plugins/is empty — if it already holds any.zip, bootstrap leaves it alone.
offline-plugins/vsplugins/:offline-plugins/is the input — the zips bootstrap (or you) stage — whileplugins/is the output the entrypoint unzips them into; copying zips intoplugins/does nothing. If you keep the plugin zips outside the three auto-staged locations, stage them manually first:sudo mkdir -p /opt/grafana/prometheus-grafana-stack/grafana/data/offline-plugins sudo cp /tmp/docker-images-bundle-grafana-plugins/*.zip \ /opt/grafana/prometheus-grafana-stack/grafana/data/offline-plugins/ sudo chown -R 472:472 /opt/grafana/prometheus-grafana-stack/grafana/dataThe
chownmatters — Grafana runs as uid472and must be able to write undergrafana/data.For Oracle deployments, also extract the Oracle datasource plugin (normally downloaded by bootstrap from GitHub):
sudo mkdir -p /opt/grafana/prometheus-grafana-stack/grafana/data/plugins/albertowd-oraclegrafana-datasource sudo tar -xzf /tmp/docker-images-bundle-grafana-plugins/albertowd-oraclegrafana-datasource.tar.gz \ -C /opt/grafana/prometheus-grafana-stack/grafana/data/plugins/albertowd-oraclegrafana-datasource
Tips:
-
TROUBLESHOOTING: Run
sudo bash /opt/grafana/misc/diagnose.shto automatically check Docker, containers, ports, firewall, and Prometheus targets -
Environment Configuration: All deployments use environment variables (
.envfiles) for secure and flexible configuration
Pre-Deployment Checklist
Before starting, gather the following information:
-
Prometheus Server IP - Will be used in firewall rules on all nodes
-
SMTP Credentials (optional) - For Grafana alerting (host, port, user, password)
-
MySQL Credentials - If deploying MySQL exporter (user, password)
-
Oracle Credentials - If deploying Oracle exporter (user, password)
-
Server IPs - All servers that will be monitored
For clarity in these instructions, we use the following servers:
-
acs1,acs2,hc1,hc2,db,iis
In this example, Prometheus is installed on the same server as the DB. You need to know its IP address during setup to whitelist it on the servers where the exporters are installed:
ACS1 - 65.109.58.164 ACS2 - 65.109.58.100 HC1 - 65.109.24.154 HC2 - 65.108.67.167 DB - 65.109.58.165 <- PROMETHEUS_IP IIS - 65.109.49.150
1. Install Docker and Docker Compose on All Linux Servers
Target servers: acs1, acs2, hc1, hc2, db, *-api servers
Before installing exporters, ensure that Docker and Docker Compose are installed on all target servers (except Windows-based servers).
Automated Installation (Recommended)
Use the automated installation script:
# If repository is already cloned:
cd /opt/grafana
sudo bash /opt/grafana/misc/install-docker.sh
# Or via one-command bootstrap (installs Docker automatically):
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- PROMETHEUS_IP
The script automatically:
-
✅ Detects your OS and package manager
-
✅ Fixes EOL repos (CentOS 7/8)
-
✅ Installs Docker CE from official repository
-
✅ Installs Docker Compose plugin (falls back to static binary if needed)
-
✅ Enables and starts Docker service
-
✅ Verifies installation
Verify Installation
docker -v
docker compose version
Expected output (versions may vary):
Docker version 28.0.1, build 068a01e Docker Compose version v2.33.1
Manual Installation (if script fails)
Ubuntu/Debian:
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update && sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo systemctl enable --now docker
2. Install Node Exporter and Process Exporter
Target servers: acs1, acs2, hc1, hc2, db, *-api servers
Quick Installation (Recommended)
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
--exporterflag: Specifies which exporters to deploy fromexporters/folder. Usebase(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 needsjmx-hazelcast(JMX :9110 → :9101). Thejmx-wildflydirectory holds two services: the HikariCP exporter (5556) starts always, while the JVM exporter (5557) sits behind thejvmcompose 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 |
|
|
Hazelcast |
(native prometheus) |
|
MySQL/MariaDB |
|
|
Oracle DB |
|
|
ClickHouse |
|
|
PostgreSQL |
|
|
Nginx (Angular UI) |
|
Docker container |
FT Device Network |
(native endpoint) |
Docker container |
AI Agent |
(native endpoint) |
Docker container |
UI Backend |
(native endpoint) |
Docker container |
Provision API |
(native endpoint) |
Docker container |
Northbound API |
(native endpoint) |
Docker container |
Service API |
(native endpoint) |
Docker container |
(always) |
|
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 |
|
System metrics |
Process Exporter |
|
Host process metrics |
cAdvisor |
|
Docker container metrics |
MySQL Exporter |
|
MySQL database metrics |
Oracle DB Exporter |
|
Oracle database metrics |
ClickHouse (native) |
|
ClickHouse database metrics |
PostgreSQL Exporter |
|
PostgreSQL database metrics |
Nginx Exporter |
|
Nginx web server metrics |
JMX Exporter |
|
JMX metrics (Hikari) |
JMX Exporter |
|
JVM metrics |
If you need to override these ports (e.g., due to conflicts or multi-instance deployment):
-
Uncomment the relevant
--web.listen-address=…orcommand: […]line indocker-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.
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
-
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.envfile, the default values will be used automatically.-
Start the exporters using Docker Compose:
-
docker compose up -d
Hint: You can check the logs of the started containers:
docker compose logs -f
-
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 onlycom_hazelcast_Metrics_*(noisClusterSafe), so the dashboard shows “No data”. Deploy thejmx-hazelcastexporter 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_METRICStarget already scrapes on :8080 — measured on 6.5.2 and 7.1.1, 98 metric families includingjvm_memory_used_bytes,jvm_gc_*andhikaricp_connections_*. Deployingjmx-wildflythere 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 -1answers with# HELPon 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) |
|
5556 / 5557 |
HikariCP pool / JVM heap/GC/threads |
FTACS 6.5+ |
(none — Actuator on :8080) |
8080 |
Same data, via |
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
--exporterflag specifies which exporters to deploy fromexporters/folder.
base- always required (node-exporter + process-exporter + cAdvisor)
jmx-wildfly,mysql,oracle- service-specific exportersHazelcast: embedded-in-ACS uses the native Prometheus endpoint (:9101); standalone/docker needs the
jmx-hazelcastexporter (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_PORTenv var) -
Hazelcast Prometheus: port 9101 — embedded-in-ACS: native endpoint (full MBean set). Standalone/docker: served by the
jmx-hazelcastexporter scraping JMX :9110 (native alone omitsisClusterSafe).
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
Quick Installation (Recommended)
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):
-
Open
/opt/grafana/exporters/mysql/docker-compose.yml -
Uncomment and modify the
--web.listen-address=:PORTparameter in thecommand:block:
# Uncomment to override default port (9104)
# - '--web.listen-address=:19104'
4.1 Setup Steps
-
Create an exporter user in MySQL: Log in as
rootoradminand run the following commands (generate a secure password and replaceStR0nG_PassW0rD_hErEaccordingly):
CREATE USER IF NOT EXISTS 'exporter'@'%' IDENTIFIED BY 'StR0nG_PassW0rD_hErE' WITH MAX_USER_CONNECTIONS 3; GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'exporter'@'%';
-
Clone or download this repo into
/opt/grafana, in the same way as described in step 2. -
Configure environment variables: Navigate to the MySQL exporter directory and create the
.envfile:
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-
Start the exporter using Docker Compose:
-
docker compose up -d
-
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
Quick Installation (Recommended)
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
-
Clone or download this repo into
/opt/grafana, in the same way as described in step 2. -
Navigate to the Oracle exporter directory:
cd /opt/grafana/exporters/oracle/
-
Configure environment variables: Create the
.envfile 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-
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
-
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-
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:
-
Prometheus target shows UP but dashboard shows No data
-
curl http://localhost:<port>/metricshangs from host -
docker exec <container> curl http://localhost:<port>/metricsworks fine inside the container -
Port is listening (
ss -tlnpshowsdocker-proxy) but no data comes through
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: hostputs every one of the service’s ports on all interfaces, including the public one — there is noports:list left to restrict. For Hazelcast that means5701and the JMX port face the internet, and an open Hazelcast member port is a known remote-code-execution vector. Firewall them in theDOCKER-USERchain (UFW alone does not apply to Docker traffic — seedocs/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.1in environment variables: -
HZ_MEMBERS=hazelcast:5701→HZ_MEMBERS=127.0.0.1:5701 -
HZ_PUBLIC_ADDRESS=hazelcast:5711→HZ_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.1or 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: hostand 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 |
|
Angular backend API |
ft-device-network |
Spring Boot |
8383 |
|
WiFi mesh service |
ui-ai-agent |
Python/FastAPI |
8084 |
|
AI assistant |
clickhouse |
Database |
9363 |
|
Analytics database |
postgres |
Database |
5432 |
via exporter |
AI agent database |
Quick Installation (Recommended)
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
-
Download from Windows Exporter Releases
-
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
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
NODEtype since it exposes metrics on the same port (9100) in a compatible format. Theservice=IISlabel is used by the Windows/IIS Grafana dashboard for filtering.
8. Configure and Deploy Prometheus with Grafana
Target server: db (or separate server dedicated for Grafana only)
In this example, we are using the db server to host Grafana and Prometheus, but there is no strict requirement—it can be any server that suits your environment.
Hardware requirements: For small-scale setups (up to 10 server nodes), 4 vCPU and 8 GB RAM is sufficient. For larger deployments, see Grafana sizing guidelines.
Quick Installation (Recommended)
Use the one-command bootstrap:
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- stack
On the interactive path, bootstrap.sh stack opens a full-screen arrow-key
configuration menu (↑↓ + Enter to navigate, Esc to go back, per-item help
panel, masked passwords; nothing is written until Apply):
| Menu section | What it configures |
|---|---|
Server |
|
Prometheus |
port, data retention, basic-auth user/password (protects the Prometheus/Alertmanager UIs) |
Grafana |
port, admin user/password, and the shared SMTP (host/from/user/password) used by both Grafana and alert email |
FTACS Database |
MySQL/Oracle type + host/port/user/password/name for the Business dashboard (empty host = skip) |
SSL / HTTPS |
self-signed (validity/org), Let’s Encrypt (email), or an existing certificate (paths shown) |
Alerting |
drill into any channel to configure it — Email, Telegram (live chat auto-detect), Slack, Teams, Webhook, SMS, SNMP (v2c/v3) |
UI Portal integration |
|
Review & apply |
shows every value, then writes |
After Apply the script deploys the stack (containers up), auto-registers
this host’s exporters in servers.env and generates targets, creates the
read-only Grafana service token, and — on an existing install — changes the
admin password via grafana cli if you set a new one (no current password
needed). Values live in /opt/grafana/prometheus-grafana-stack/.env.
Run with -y to skip the menu entirely (non-interactive: domain = server
IP, random passwords, self-signed cert).
After deployment:
-
Configure Prometheus targets (see section 8.1 below)
-
Verify
/grafana-ro/works if you configured UI Portal Origins in phase 5
Manual Installation (click to expand)
-
Clone or download this repo into
/opt/grafana, as described previously. -
Configure environment variables: Navigate to the Prometheus-Grafana stack directory and create the
.envfile:
cd /opt/grafana/prometheus-grafana-stack cp .env.example .env vi .env
Update the following REQUIRED variables:
# Server Configuration (REQUIRED)
SERVER_IP=65.109.58.165 # Replace with your server's IP
# Grafana Admin Credentials (REQUIRED)
# NOTE: Password in .env is used only on FIRST deployment.
# After that, database is the source of truth.
# To change password later, use: docker exec grafana grafana cli admin reset-admin-password NEW_PASS
GRAFANA_ADMIN_USER=admin
GRAFANA_ADMIN_PASSWORD=your_secure_password_here
# SMTP Configuration (REQUIRED for alerting)
SMTP_ENABLED=true
SMTP_HOST=smtp.example.com:587
SMTP_USER=your_smtp_user
SMTP_PASSWORD=your_smtp_password
# Grafana Service Token (REQUIRED for nginx proxy)
GRAFANA_SERVICE_TOKEN=your_token_here # Will be generated in section 8.2
Optional variables (can be left as default):
-
PROMETHEUS_VERSION,GRAFANA_VERSION,NGINX_VERSION -
PROMETHEUS_PORT=9090— Prometheus’s own listen port.GRAFANA_PORT=443— the external HTTPS port nginx listens on for Grafana, not Grafana’s own port (Grafana listens on127.0.0.1:3100, hardcoded indocker-compose.yml; there is no variable for it) -
PROMETHEUS_RETENTION=90d -
GRAFANA_PLUGINS- Comma-separated list of plugins Full environment variables reference
Variable |
Description |
Required |
|
Your server’s external IP address or hostname |
✅ Yes |
|
Admin username for Grafana authentication |
✅ Yes |
|
Admin password for Grafana authentication |
✅ Yes |
|
SMTP server address and port for email alerts (e.g., smtp.gmail.com:587) |
✅ Yes |
|
SMTP username (usually an email address) |
✅ Yes |
|
SMTP password or API key |
✅ Yes |
|
Service account token for nginx proxy (generated in section 8.2) |
✅ Yes |
|
Data retention period (default: 90d) |
❌ No |
|
Prometheus port (default: 9090) |
❌ No |
|
External HTTPS port nginx listens on for Grafana (default: 443) — not Grafana’s own port, which is hardcoded to |
❌ No |
For a complete list of all variables, see .env.example in each directory.
Grafana Internal Environment Variables Reference:
These variables are set automatically by docker-compose based on your .env file:
| Variable | Description |
|---|---|
|
Root URL of Grafana instance, used to generate correct links in the UI |
|
Allowed origin for CORS (Cross-Origin Resource Sharing) |
|
Allowed origins for WebSocket connections (Grafana Live) |
|
Admin username for Grafana authentication |
|
Admin password for Grafana authentication |
|
Enables SMTP email notifications ( |
|
SMTP server address and port for sending alerts |
|
SMTP username (usually an email address) |
|
SMTP password or API key |
|
Allows embedding dashboards in iframes ( |
|
Enables JSON-based dashboard provisioning |
|
Custom CSS file path for UI styling |
-
Prepare generated files (the bootstrap script creates these automatically — a manual/offline installation must create them before the first start, otherwise Docker turns the missing bind-mount files into directories and the containers fail with “Are you trying to mount a directory onto a file”):
cd /opt/grafana/prometheus-grafana-stack
# Alertmanager config placeholder (regenerated by the container entrypoint)
cat > alertmanager/alertmanager.yml << 'EOF'
# Placeholder - will be regenerated by alertmanager entrypoint.sh
global:
resolve_timeout: 5m
route:
receiver: 'default'
receivers:
- name: 'default'
EOF
chmod 666 alertmanager/alertmanager.yml
# TLS certificate for the nginx proxy (self-signed; replace with a real
# certificate later if you have one — files: ssl/certificate.crt, ssl/private.key).
# ssl/ is git-ignored, so it is NOT in the ZIP — create it first.
mkdir -p ssl
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout ssl/private.key -out ssl/certificate.crt \
-subj "/C=US/ST=State/L=City/O=FriendlyTech/CN=${GRAFANA_DOMAIN:-$(hostname -f)}"
# Prometheus basic-auth file for nginx (user: prometheus). Also git-ignored, so
# nginx would otherwise mount a directory onto it and fail. htpasswd may be
# absent on RHEL — fall back to openssl apr1; write the password back into .env.
PROM_PASS=$(openssl rand -base64 12 | tr -dc 'a-zA-Z0-9' | head -c16)
if command -v htpasswd >/dev/null 2>&1; then
htpasswd -bc .htpasswd prometheus "$PROM_PASS"
else
echo "prometheus:$(openssl passwd -apr1 "$PROM_PASS")" > .htpasswd
fi
grep -q '^PROMETHEUS_PASSWORD=' .env \
&& sed -i "s/^PROMETHEUS_PASSWORD=.*/PROMETHEUS_PASSWORD=$PROM_PASS/" .env \
|| echo "PROMETHEUS_PASSWORD=$PROM_PASS" >> .env
echo "Prometheus UI login: prometheus / $PROM_PASS"
# nginx ACME webroot (bind-mounted; keep it a directory)
mkdir -p acme-challenge
Also make sure GRAFANA_DOMAIN in .env is set to the server’s domain or IP — nginx serves Grafana only for that hostname.
. Start the Prometheus-Grafana stack:
cd /opt/grafana/prometheus-grafana-stack
docker compose up -d
-
Enable access to Grafana and Prometheus ports:
iptables -A INPUT -p tcp --dport 80 -j ACCEPT iptables -A INPUT -p tcp --dport 443 -j ACCEPT # If server has IPv6 address, also add ip6tables rules: ip6tables -A INPUT -p tcp --dport 80 -j ACCEPT ip6tables -A INPUT -p tcp --dport 443 -j ACCEPT
Note: Ports 9090 (Prometheus) and 3100 (Grafana) bind to
127.0.0.1and are only accessible through nginx reverse proxy. No need to open them in firewall.
-
Test the setup:
-
Prometheus:
https://<domain>/prometheus/targets— all endpoints should be in UP state (login:prometheus/.env→PROMETHEUS_PASSWORD) -
Grafana:
https://<domain>/dashboards— (login:admin/.env→GRAFANA_ADMIN_PASSWORD)
-
-
Configure Alerting (optional): Update the email in
/opt/grafana/prometheus-grafana-stack/grafana/provisioning/alerting/alert_resources.yamlwith your email(s). Multiple emails can be separated by;,\n, or,.
Accessing Web Interfaces
After deployment, the monitoring stack provides two web interfaces accessible via nginx reverse proxy:
| Service | URL | Username | Password |
|---|---|---|---|
Grafana |
|
|
|
Prometheus |
|
|
Note: Both services bind to
127.0.0.1and are only accessible through nginx. Direct access viaIP:3100orIP:9090is not available.
Note: The “Business metrics” dashboard requires connection to your FTACS application database (MySQL or Oracle) for tenant/domain lists. Configure
FTACS_DB_*variables in.env(setFTACS_DB_TYPEtomysqlororacle) — this is the ACS database, not the monitoring database.
How it works:
All external traffic goes through nginx reverse proxy (ports 80/443). Nginx handles SSL, authentication, and forwards requests to internal services:
Browser → https://<domain>/ → nginx (port 443) → Grafana (127.0.0.1:3100) Browser → https://<domain>/prometheus → nginx (port 443) → Prometheus (127.0.0.1:9090)
Grafana and Prometheus listen only on 127.0.0.1 (localhost) — they are not reachable directly from outside. This is why only ports 80 and 443 need to be opened in the firewall, not 3100 or 9090.
How to find your passwords:
cd /opt/grafana/prometheus-grafana-stack
# View Grafana password
grep GRAFANA_ADMIN_PASSWORD .env
# View Prometheus password
grep PROMETHEUS_PASSWORD .env
# Reset Grafana password if forgotten
docker exec grafana grafana cli admin reset-admin-password NEW_PASSWORD
Grafana UI pages:
-
Dashboards:
https://<domain>/dashboards -
Alerting:
https://<domain>/alerting/list -
Data sources:
https://<domain>/connections/datasources
Prometheus UI pages:
-
Targets status:
https://<domain>/prometheus/targets— verify all endpoints show UP -
Query metrics:
https://<domain>/prometheus/graph— execute PromQL queries -
Configuration:
https://<domain>/prometheus/config— view active scrape configs
8.1 Configure Prometheus Targets
After the stack is running, configure which servers Prometheus should monitor.
Tip: When you deploy exporters using
quick-deploy.shorbootstrap.sh, the script outputs ready-to-use configuration lines forservers.env. Just copy and paste them.
The stack server registers itself. Re-running
bootstrap.sh stackappends this host’s own exporters if they are not listed yet, so it is safe to run repeatedly.NODE,PROCESSandCADVISORdescribe the machine, so there is exactly one of each per host: an entry already counts as present if either its address or its instance name matches. That matters because both can change under you — a new NIC or DHCP lease moves the address, and a differentINSTANCE_PREFIXrenames the instance. Matching on one key alone would let the other change through as a brand-new target, and the host would be scraped twice under two names, doubling every host-level panel. Nothing is ever rewritten in place: when the existing line disagrees with what was just detected, bootstrap prints both spellings and leaves the file alone, e.g.⚠ servers.env: NODE already listed as '10.0.0.30:9100 DB-MySQL' for this host (detected: 10.0.0.30:9100 stack-DB-MySQL) — not duplicating; edit the line if it is out of dateEdit the line yourself if the old value is wrong, then re-run
generate-targets.sh.
Step 1: Edit the server inventory
cd /opt/grafana/prometheus-grafana-stack/prometheus/targets
vi servers.env
First time setup:
Run
/opt/grafana/prometheus-grafana-stack/prometheus/scripts/generate-targets.sh— createsservers.envfrom templateEdit
servers.env— add your serversRun
/opt/grafana/prometheus-grafana-stack/prometheus/scripts/generate-targets.shagain — generates JSON target filesYour
servers.envis git-ignored and won’t be overwritten by repository updates.
Format: TYPE IP:PORT INSTANCE [EXTRA_LABELS]
| Field | Description | Example |
|---|---|---|
|
Exporter type (see supported types below) |
|
|
Server IP and exporter port |
|
|
Unique instance name for Grafana |
|
|
Optional comma-separated Prometheus labels |
|
EXTRA_LABELS are additional labels attached to all metrics from this target. Useful for:
-
Filtering in Grafana dashboards (
service=mysql) -
Grouping servers by role (
role=database,role=application) -
Environment tagging (
env=prod,env=staging)
Instance Naming Convention (recommended):
Instance names are flexible - you can use any naming scheme that works for your organization. The recommended format is {Environment}-{ServerType}{Number}:
| Prefix | Server Role | Examples |
|---|---|---|
|
Application servers (WildFly/FTACS) |
|
|
Database servers (MySQL, Oracle) |
|
|
Hazelcast cache servers (standalone) |
|
|
Microservices (Provision, Northbound, Service, Subscription) |
|
|
Windows/IIS web servers |
|
💡 Tip: Use the
rolelabel for server grouping:role=database,role=application,role=cache,role=api,role=webserver,role=monitoring
Which exporters for which server type:
| Server Type | Required Exporters | Optional Exporters |
|---|---|---|
Any server |
|
- |
MySQL DB |
|
- |
Oracle DB |
|
- |
ClickHouse |
|
- |
PostgreSQL |
|
- |
WildFly/ACS |
|
|
Hazelcast (standalone) |
|
- |
Angular UI (HC1) |
|
|
API servers |
|
- |
FTACS servers |
|
- |
Windows/IIS |
|
- |
Minimal example (one server of each type):
# MySQL database server
NODE 10.0.0.10:9100 Prod-DB
PROCESS 10.0.0.10:9256 Prod-DB
CADVISOR 10.0.0.10:9183 Prod-DB
MYSQL 10.0.0.10:9104 Prod-DB
# WildFly/ACS application server
NODE 10.0.0.20:9100 Prod-ACS1
PROCESS 10.0.0.20:9256 Prod-ACS1
CADVISOR 10.0.0.20:9183 Prod-ACS1
JMX_HIKARI 10.0.0.20:5556 Prod-ACS1
JMX_JVM 10.0.0.20:5557 Prod-ACS1
# Hazelcast cache server (standalone)
NODE 10.0.0.30:9100 Prod-HC1 role=cache
PROCESS 10.0.0.30:9256 Prod-HC1
CADVISOR 10.0.0.30:9183 Prod-HC1
JMX_HC 10.0.0.30:9101 Prod-HC1
# API server (microservices)
NODE 10.0.0.35:9100 Prod-API1 role=api
PROCESS 10.0.0.35:9256 Prod-API1
CADVISOR 10.0.0.35:9183 Prod-API1
PROVISION_API 10.0.0.35:8091 Prod-API1
NORTHBOUND_API 10.0.0.35:9880 Prod-API1
SERVICE_API 10.0.0.35:8085 Prod-API1
SUBSCRIPTION_API 10.0.0.35:8080 Prod-API1
# Oracle database server
NODE 10.0.0.40:9100 Prod-OracleDB
PROCESS 10.0.0.40:9256 Prod-OracleDB
CADVISOR 10.0.0.40:9183 Prod-OracleDB
ORACLE 10.0.0.40:9161 Prod-OracleDB
# Windows/IIS web server
NODE 10.0.0.50:9100 Prod-IIS service=IIS,role=webserver
Full example with labels and API endpoints
# === DATABASE SERVERS ===
NODE 65.109.58.165:9100 DevOps-DB service=mysql,role=database
PROCESS 65.109.58.165:9256 DevOps-DB
CADVISOR 65.109.58.165:9183 DevOps-DB
MYSQL 65.109.58.165:9104 DevOps-DB
# === ACS SERVERS ===
NODE 65.109.58.164:9100 DevOps-ACS1 service=ACS,role=application
PROCESS 65.109.58.164:9256 DevOps-ACS1
CADVISOR 65.109.58.164:9183 DevOps-ACS1
JMX_HIKARI 65.109.58.164:5556 DevOps-ACS1
JMX_JVM 65.109.58.164:5557 DevOps-ACS1
# Optional ACS endpoints:
FT_SYSTEM 65.109.58.164:8090 DevOps-ACS1
ACS_METRICS 65.109.58.164:8080 DevOps-ACS1
JMX_HC 65.109.58.164:9101 DevOps-ACS1
# === HAZELCAST SERVERS (standalone) ===
NODE 65.109.24.154:9100 DevOps-HC1 service=hazelcast,role=cache
PROCESS 65.109.24.154:9256 DevOps-HC1
CADVISOR 65.109.24.154:9183 DevOps-HC1
JMX_HC 65.109.24.154:9101 DevOps-HC1
# === API SERVERS (microservices) ===
NODE 65.109.24.155:9100 DevOps-API1 service=api,role=api
PROCESS 65.109.24.155:9256 DevOps-API1
CADVISOR 65.109.24.155:9183 DevOps-API1
PROVISION_API 65.109.24.155:8091 DevOps-API1
NORTHBOUND_API 65.109.24.155:9880 DevOps-API1
SERVICE_API 65.109.24.155:8085 DevOps-API1
SUBSCRIPTION_API 65.109.24.155:8080 DevOps-API1
# === ORACLE SERVERS ===
NODE 65.109.20.174:9100 DevOps-OracleDB service=oracle,role=database
PROCESS 65.109.20.174:9256 DevOps-OracleDB
CADVISOR 65.109.20.174:9183 DevOps-OracleDB
ORACLE 65.109.20.174:9161 DevOps-OracleDB
# === IIS SERVERS ===
NODE 65.109.49.150:9100 devops-ui.friendly-tech.com service=IIS,role=webserver,node_ip=65.109.49.150
Step 2: Generate Prometheus target files
cd /opt/grafana/prometheus-grafana-stack/prometheus
/opt/grafana/prometheus-grafana-stack/prometheus/scripts/generate-targets.sh
The script automatically:
-
✅ Parses
servers.env -
✅ Generates JSON files in
targets/folder (one per exporter type) -
✅ Adds
exporter_typelabel to each target (e.g.,node,hazelcast,hikari) -
✅ Validates JSON syntax
-
✅ Checks target reachability
-
✅ Shows summary of generated targets
Note: The script validates all entries before generating targets. If there are format errors (wrong TYPE, missing instance name, labels in instance field), it will show the exact line, what’s wrong, and how to fix it.
How it works:
prometheus.ymlusesfile_sd_configsto read JSON files fromtargets/folder. Prometheus auto-reloads within 30 seconds — no restart needed!
Adding new servers later:
-
Edit
servers.env— add new server lines -
Run
/opt/grafana/prometheus-grafana-stack/prometheus/scripts/generate-targets.sh -
Done! Prometheus picks up changes automatically
Benefits:
-
✅ Zero downtime - No Prometheus restart required
-
🛡️ Error-proof - Cannot break YAML syntax
-
📝 Simple format - Easy to read and edit
-
🔄 Auto-reload - Prometheus picks up changes automatically
-
🔒 Safe updates - Your
servers.envis git-ignored and won’t be overwritten
Supported target types (TYPE field reference)
The TYPE field in servers.env determines which Prometheus job will scrape the target. Each type maps to a specific exporter and port:
| TYPE | Port | Description | Prometheus Job | exporter_type |
|---|---|---|---|---|
|
9100 |
System metrics (CPU, memory, disk) |
|
|
|
9256 |
Host process metrics |
|
|
|
9183 |
Docker container metrics |
|
|
|
9104 |
MySQL/MariaDB database metrics |
|
|
|
9161 |
Oracle database metrics |
|
|
|
9363 |
ClickHouse database metrics (native endpoint) |
|
|
|
9187 |
PostgreSQL database metrics |
|
|
|
9113 |
Nginx web server metrics (Angular UI) |
|
|
|
5556 |
HikariCP connection pool (WildFly) |
|
|
|
5557 |
JVM heap, GC, threads (WildFly) |
|
|
|
9101 |
Hazelcast: native endpoint (embedded in ACS) or the |
|
|
|
8090 |
FT System Spring Boot metrics (WildFly) |
|
|
|
8080 |
ACS application metrics |
|
|
|
8383 |
FT Device Network Service (Actuator) |
|
|
|
8084 |
AI Agent Python/FastAPI metrics |
|
|
|
8881 |
UI Backend Spring Boot Actuator |
|
|
|
8091 |
Provision Portal API metrics |
|
|
|
9880 |
Northbound API metrics |
|
|
|
8085 |
Service API metrics |
|
|
|
8080 |
Subscription API metrics |
|
|
Parking a target that stopped answering. generate-targets.sh probes every
row and reports the ones that did not respond. Two flags act on that list —
neither ever deletes a line, because a row records an address somebody added for
a reason and deleting it takes the reason with it:
# show what did not answer this run; changes nothing
bash /opt/grafana/prometheus-grafana-stack/prometheus/scripts/generate-targets.sh --park-dry-run
# comment those rows out, keeping the original line, with a backup
bash /opt/grafana/prometheus-grafana-stack/prometheus/scripts/generate-targets.sh --park
A parked row looks like this, and comes back by deleting the three #:
# PARKED 2026-08-05 - did not answer when targets were last generated # JMX_HC 135.181.62.187:9101 ACS-qa12m # -> uncomment when JMX_HC answers again at 135.181.62.187:9101
Parking never happens on its own. A target that is merely restarting reads as
unreachable too, so --park has to be typed by someone who has looked at the
dry-run list. A row that answered is never touched.
Metrics paths. Most targets are scraped on /metrics, but the Spring Boot
services are not, and the path is not guessable from the service name. These are
the paths the generator writes into metrics_path — use them when probing a
target by hand:
-
NORTHBOUND_APIandSERVICE_API—/iot-webservice/actuator/prometheus -
PROVISION_API—/prov-portal/actuator/prometheus -
FT_DEVICE_NETWORK—/api/actuator/prometheus -
SUBSCRIPTION_API—/rest/subscription-metrics(ACS 6.4 only; 6.5+ serves these throughACS_METRICSinstead, and the generator skips the target when the endpoint answers without metrics) -
FT_SYSTEM,UI_BACKEND,AI_AGENT,ACS_METRICS—/actuator/prometheus
So a target that looks down is worth re-checking on its own path before anything else:
curl -s -o /dev/null -w '%{http_code}\n' \
http://<ip>:8091/prov-portal/actuator/prometheus # not /metrics
Note: The port in the table is the default. Always specify the actual port in your
servers.envline:TYPE IP:PORT INSTANCE
How IP addresses appear in Grafana dashboards
No configuration needed — prometheus.yml already includes relabel_configs that extract IP addresses from target addresses and store them in the node_ip label.
Grafana dashboards use this label to display server IPs in tables and filters.
How it works (FYI):
# prometheus.yml already has this for each job:
relabel_configs:
- source_labels: [__address__]
target_label: node_ip
regex: (.+):.* # extracts IP from "IP:PORT"
replacement: $1
8.2 Configure Nginx Proxy for Embedding
Enable a read-only reverse proxy for embedding Grafana dashboards in external applications (e.g., Angular UI portal).
The /grafana-ro/ endpoint requires two things configured in .env:
-
GRAFANA_SERVICE_TOKEN— Grafana service account token (auto-generated by bootstrap, or create manually — see below) -
UI_PORTAL_ORIGINS— comma-separated list of portal URLs allowed to access/grafana-ro/(IP whitelist)
Step 1: Configure allowed portal origins
Set UI_PORTAL_ORIGINS in .env to the URLs of applications that will embed Grafana:
cd /opt/grafana/prometheus-grafana-stack
vi .env
# Example: allow access from Angular UI portal
UI_PORTAL_ORIGINS=https://portal.example.com,https://10.0.0.50:8880
Without this setting, /grafana-ro/ returns 403 Forbidden for all external requests.
Step 2: Verify service token
Check that GRAFANA_SERVICE_TOKEN is set (not CHANGE_ME):
grep GRAFANA_SERVICE_TOKEN .env
If it shows CHANGE_ME, generate a token — see “Manual token generation” below.
Step 3: Restart containers
docker compose down && docker compose up -d
Test the Nginx reverse proxy:
Open https://grafana_domain/grafana-ro/ — you should see Grafana dashboards without needing to log in, with read-only permissions.
Manual token generation (if bootstrap failed or manual installation)
Run the automated script:
cd /opt/grafana
sudo bash /opt/grafana/misc/generate-grafana-token.sh --restart
The script creates a Grafana service account (ReadOnlySA) with Viewer role, generates a token, and updates .env automatically.
If the script fails, generate the token manually:
-
Open Grafana: https:/// (Grafana binds to
127.0.0.1only — it is reachable through nginx, not directly via the server IP) -
Go to: Administration → Users and access → Service accounts
-
Click “Add service account” → Name:
ReadOnlySA, Role:Viewer→ Add -
Click “Add service account token” → Name:
nginx_ro→ Copy the token -
Update
.env:
cd /opt/grafana/prometheus-grafana-stack vi .env # Set: GRAFANA_SERVICE_TOKEN=glsa_YourActualTokenHere_12345678
-
Restart:
docker compose down && docker compose up -d
How nginx proxy works
The nginx container uses a template file (nginx.conf.template) with ${GRAFANA_SERVICE_TOKEN} placeholder. On startup, the entrypoint script:
-
Reads the template
-
Replaces placeholder with value from
.env -
Generates final
nginx.conf -
Starts nginx
All services run in network_mode: host. Nginx connects to Grafana via http://127.0.0.1:3100 and serves HTTPS on ports 80/443.
8.3 Embedding Dashboards in iframe
The /grafana-ro/ path provides read-only access without authentication, making it perfect for embedding dashboards in external applications.
How to get dashboard URL for embedding:
-
Open the dashboard in Grafana (via
https://<your-domain>/, with authentication) -
Copy the dashboard path (e.g.,
/d/abc123/my-dashboard) -
Add parameters:
-
?orgId=1- Organization ID (required) -
&kiosk- Kiosk mode (hides Grafana menus) -
&refresh=30s- Auto-refresh interval (optional)
-
Example iframe code:
<iframe
src="/d/business-metrics/business-metrics?orgId=1&kiosk&refresh=30s"
width="100%"
height="600"
frameborder="0">
</iframe>
Available kiosk modes:
-
&kiosk- Full kiosk mode (no top nav, no side menu) -
&kiosk=tv- TV mode (hides top nav, shows only dashboard)
Common parameters:
-
&from=now-6h&to=now- Time range -
&var-instance=ServerName- Dashboard variable values -
&theme=dark- Dark theme -
&theme=light- Light theme
Important notes:
-
Do NOT use “Share externally” button in Grafana UI (requires additional permissions)
-
Service account with Viewer role cannot create shared dashboards
-
Always use direct dashboard URLs through
/grafana-ro/path -
Test the URL in browser before embedding to ensure it works
Verify nginx proxy is working:
# Should return 200 OK without authentication
curl -kI https://YOUR_SERVER_DOMAIN/grafana-ro/
9. Managing Grafana Dashboards Provisioning
By default, Grafana automatically provisions all dashboards located under:
/opt/grafana/prometheus-grafana-stack/grafana/provisioning/dashboards/definitions/
Sometimes it is not necessary (or desirable) to install all dashboards at once. You have two options:
9.1 Skip Dashboards During Installation
-
Before running the stack, remove or move unwanted dashboard JSON files from the definitions folder:
mkdir -p /opt/grafana/prometheus-grafana-stack/grafana/provisioning/dashboards/disabled mv /opt/grafana/prometheus-grafana-stack/grafana/provisioning/dashboards/definitions/<dashboard-to-skip>.json \ /opt/grafana/prometheus-grafana-stack/grafana/provisioning/dashboards/disabled/
Grafana will only load dashboards present in the definitions folder during startup.
-
Start the stack as usual:
cd /opt/grafana/prometheus-grafana-stack docker compose up -d
9.2 Add Dashboards Later
If later you decide to enable some dashboards:
-
Move the desired JSON file(s) back into the definitions folder:
mv /opt/grafana/prometheus-grafana-stack/grafana/provisioning/dashboards/disabled/<dashboard>.json \ /opt/grafana/prometheus-grafana-stack/grafana/provisioning/dashboards/definitions/
-
Restart Grafana container to apply changes:
cd /opt/grafana/prometheus-grafana-stack docker compose restart grafana
Tip: You don’t need to restart the whole stack, only the Grafana container.
10. Alertmanager Configuration
Alertmanager handles alert notifications from Prometheus. It supports multiple notification channels.
API Documentation: See Alertmanager API Guide for application integration (send alerts, manage silences, query status).
SNMP Integration: See SNMP Notifier Guide for NMS integration (custom OIDs, MIB file, SNMPv3 configuration).
Supported Channels
| Channel | Required Settings | Description |
|---|---|---|
SMTP + ALERT_EMAIL |
Uses SMTP settings from Grafana configuration |
|
Bot Token + Chat ID |
Auto-detection supported |
|
Webhook URL |
Incoming Webhook from Slack App |
|
Webhook URL |
Power Automate Workflows |
|
URL |
Custom HTTP endpoint |
|
Gateway URL + Token |
HTTP gateway (Twilio, Vonage) |
|
Trap Address + Version |
SNMP v2c/v3 traps to NMS |
Interactive Setup
Alertmanager is configured during stack deployment:
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- stack
# Configure alert notifications? [y/N]: y
# Select channels: 1,2 (Email + Telegram)
The bootstrap script will:
-
Guide you through each channel configuration
-
Offer testing options (MailPit for email, SNMP Trap Catcher for SNMP)
-
Auto-detect Telegram chat_id when possible
-
Validate input format
-
Save settings to
.env
ALERT_EMAIL=alerts@company.com
During bootstrap, you have 3 options for Email configuration:
-
Use Grafana SMTP settings - Uses SMTP already configured for Grafana
-
Configure new SMTP settings - Enter custom SMTP host, user, password
-
Use MailPit for testing - Auto-configures fake SMTP (no real emails sent)
Testing with MailPit (fake SMTP):
# Start MailPit (auto-started when option 3 is selected)
docker compose --profile testing up -d mailpit
# Web UI to view emails (opens automatically in bootstrap output)
open http://<server-ip>:8025
-
MailPit captures all emails without sending them
-
Modern HTML email templates with severity colors
-
View email history in web interface
SNMP Traps
During bootstrap, you have 2 options for SNMP configuration:
-
Configure external NMS - Enter NMS address, SNMP version, community/credentials
-
Use SNMP Trap Catcher for testing - Auto-configures local trap receiver with web UI
# SNMPv2c
SNMP_TRAP_ADDRESS=nms.company.com:162
SNMP_VERSION=V2c
SNMP_COMMUNITY=public
# SNMPv3 (authentication + encryption)
SNMP_VERSION=V3
SNMP_AUTH_USERNAME=snmpuser
SNMP_AUTH_PASSWORD=authpassword
SNMP_PRIV_PASSWORD=privpassword
Testing with SNMP Trap Catcher:
# Start SNMP services (auto-started when option 2 is selected)
docker compose --profile snmp up -d
# Web UI to view traps (opens automatically in bootstrap output)
open http://<server-ip>:8089
-
Captures all SNMP traps locally
-
Real-time web interface showing trap history
-
Supports SNMPv2c (for testing)
SNMP Documentation:
-
SNMP Notifier Guide — full setup guide, OID tables per protocol (TR-069, USP, LWM2M), SNMPv3 configuration, device limit alarms, infrastructure resource alarms
-
Architecture: SNMP Trap Flow — diagram showing Alert → Alertmanager → snmp-notifier → SNMP trap → NMS, component descriptions, OID structure
-
OID Mapping Reference — complete OID-to-alert mapping table with numeric OIDs, alert names, and descriptions
-
MIB File — ASN.1 MIB definition (FRIENDLY-TECH-ACS-MIB v2.0.0) for import into NMS systems; on an installed server:
/opt/grafana/prometheus-grafana-stack/alertmanager/mibs/FRIENDLY-TECH-ACS-MIB.txt -
PROD-468 Implementation Report — detailed report of granular OID mapping implementation, testing results, acceptance criteria coverage
-
Thresholds UI —
https://<domain>/alertmanager-config/thresholds— web interface for editing alert thresholds without restart (auto-discovers all configurable alerts) -
Alert Rules Editor —
https://<domain>/alertmanager-config/alerts— web interface for full CRUD on Prometheus alert rules (add/edit/delete alerts with OID labels)
Testing Services
If you selected testing options during bootstrap, these UIs are available:
| Service | URL | Purpose |
|---|---|---|
MailPit |
View captured emails |
|
SNMP Trap Catcher |
View captured SNMP traps |
Testing Notification Channels
When you send a test alert to Alertmanager, it routes to ALL configured channels:
Test Alert → Alertmanager (9093) → Email (MailPit if testing)
→ Telegram
→ Slack
→ MS Teams
→ Webhook
→ SNMP Notifier (9464) → NMS / Trap Catcher (162)
Step 1: Send Test Alert
curl -X POST http://localhost:9093/api/v2/alerts \
-H "Content-Type: application/json" \
-d '[{
"labels": {"alertname": "TestAlert", "severity": "warning", "instance": "test"},
"annotations": {"summary": "Test alert - please ignore"}
}]'
Step 2: Verify Each Channel
| Channel | How to Verify | Expected Result |
|---|---|---|
Check inbox of ALERT_EMAIL |
Email with alert details |
|
Telegram |
Check configured chat/group |
Message with 🟠 WARNING |
Slack |
Check configured channel |
Message in #alerts |
MS Teams |
Check configured channel |
Adaptive Card message |
Webhook |
Check your app logs |
POST request with JSON |
SNMP |
Check NMS or snmp-catcher |
Trap with OID 1.3.6.1.4.1.65666.2.0.99 |
SNMP Testing
# 1. Check SNMP services are running
docker ps | grep -E "snmp-notifier|snmp-catcher"
# 2. Send test alert
curl -X POST http://localhost:9093/api/v2/alerts \
-H "Content-Type: application/json" \
-d '[{"labels":{"alertname":"DbUnreachable","severity":"critical"}}]'
# 3. Check snmp-notifier logs
docker logs snmp-notifier --tail 10
# 4. View traps in web UI (if using SNMP Trap Catcher)
open http://<server>:8089
SNMP Trap Catcher features:
-
Web UI at port 8089 showing all received traps
-
Real-time updates
-
Trap details: timestamp, OID, variables, source IP
11. Downgrade / Rollback
Bootstrap Command Reference
# Show help
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- --help
# Show available versions
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- --version-list
Installing a Previous Version
To install a specific version, use the --branch parameter:
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- stack --branch 1.6.6
⚠️ WARNING: When specifying
--branch, you are installing a specific version instead of the latest stable release (main). Make sure this is intentional.
Before Downgrade
-
Check current version:
cat /opt/grafana/VERSION
-
List available versions:
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- --version-list
-
Backup data (recommended):
cd /opt/grafana/prometheus-grafana-stack docker compose down cp -r prometheus_data /backup/prometheus_data_$(date +%Y%m%d) cp -r grafana_data /backup/grafana_data_$(date +%Y%m%d)
Important Notes
-
Data compatibility: Downgrading may cause issues if Prometheus or Grafana database schemas changed between versions
-
Dashboards: Custom dashboards created in newer version may not work in older version
-
Configuration:
.envsettings are preserved, but new options added in later versions will be ignored
Clean Downgrade (Full Reset)
If you experience issues after downgrade or need a guaranteed clean state, perform a full reset. This involves two parts:
-
Stack cleanup (Prometheus/Grafana server) - removes metrics history, dashboards, alert states
-
Exporters cleanup (target nodes: ACS, MySQL, etc.) - removes exporter containers
Part 1: Stack (Prometheus/Grafana Server)
Step 1: Stop all services
cd /opt/grafana/prometheus-grafana-stack
docker compose down
Step 2: Backup current data (optional)
mkdir -p /backup/grafana-$(date +%Y%m%d)
cp -r prometheus_data grafana_data alertmanager_data /backup/grafana-$(date +%Y%m%d)/
cp .env /backup/grafana-$(date +%Y%m%d)/
Step 3: Remove data directories
cd /opt/grafana/prometheus-grafana-stack
rm -rf prometheus_data grafana_data alertmanager_data
Step 4: Remove repository and reinstall
rm -rf /opt/grafana
# Install latest stable version
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- stack
# Or install specific version (downgrade)
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- stack --branch 1.6.6
Note: Without
--branch, the latest stable version frommainis installed. Use--branch X.Y.Zonly when you need a specific older version.
What gets removed:
| Data | Location | Effect |
|---|---|---|
Prometheus metrics |
|
All historical metrics lost |
Grafana database |
|
Dashboards, users, API keys reset |
Alertmanager state |
|
Silences, notification history reset |
What is preserved:
| Data | Location | Notes |
|---|---|---|
Configuration |
|
Backed up in Step 2, restore manually if needed |
Target definitions |
|
Recreated by bootstrap based on detected services |
Part 2: Exporters (Target Nodes)
Run these steps on each monitored server (ACS, MySQL, etc.):
Step 1: Stop all exporter containers
cd /opt/grafana/exporters
for dir in */; do
[ -f "$dir/docker-compose.yml" ] && docker compose -f "$dir/docker-compose.yml" down
done
Step 2: Remove exporters directory
rm -rf /opt/grafana/exporters
Step 3: Reinstall exporters
# Install latest stable version
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- exporters
# Or install specific version (downgrade)
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- exporters --branch 1.6.6
What gets removed on exporter nodes:
| Component | Location | Effect |
|---|---|---|
Base exporters (node, process, cAdvisor) |
|
System/container metrics collection stops |
JMX exporters |
|
Java/WildFly metrics collection stops |
MySQL exporter |
|
Database metrics collection stops |
Note: Exporter clean install does not affect historical data - metrics are stored on the Prometheus server, not on exporter nodes.
⚠️ WARNING: Clean downgrade erases all monitoring history. Use only when:
Standard downgrade causes errors
Grafana/Prometheus fails to start after version change
You need a guaranteed working state
Capturing a Bootstrap Run
bootstrap.sh does not write log files. Session logging existed briefly and was
removed (fef34da) — no production installer writes logs to disk, so there is
no .bootstrap-logs/ directory to collect.
To capture a run for support, pipe it through tee. Only stdout is redirected,
so stdin stays a terminal and every interactive prompt still works:
bash /opt/grafana/misc/bootstrap.sh <args> 2>&1 \
| tee /tmp/bootstrap-$(date +%Y%m%d-%H%M%S).log
For a snapshot of the host rather than of one run — Docker, containers, ports, firewall and Prometheus targets — use the built-in diagnostic:
sudo bash /opt/grafana/misc/diagnose.sh
Verifying the Installation
Everything below runs entirely offline — no internet access and no tools beyond what a standard Linux host and this repository already provide.
1. Containers are up:
docker ps
Nothing should be in a Restarting state. Give nginx_proxy a specific look: a crash-looping nginx makes both Grafana and Prometheus unreachable from outside, while every exporter container next to it still shows Up and looks perfectly healthy — the outage is easy to miss if you only skim the list.
2. The built-in diagnostic:
sudo bash /opt/grafana/misc/diagnose.sh
Checks Docker, containers, ports, firewall, and Prometheus targets in one pass.
3. Every Prometheus target is up:
curl -s http://localhost:9090/api/v1/targets | python3 -c '
import json, sys
data = json.load(sys.stdin)["data"]["activeTargets"]
down = [t for t in data if t["health"] != "up"]
print("{} targets, {} not up".format(len(data), len(down)))
for t in down:
lb = t["labels"]
print(" {}: {} {}".format(t["health"], lb.get("job"), lb.get("instance")))'
4. Grafana answers:
DOMAIN=$(grep -m1 '^GRAFANA_DOMAIN=' /opt/grafana/prometheus-grafana-stack/.env | cut -d= -f2)
curl -sk -o /dev/null -w '%{http_code}\n' -H "Host: $DOMAIN" https://127.0.0.1/login
Expect 200. Two details make this check fail for the wrong reason if you shorten it:
-
The
Hostheader is required. nginx serves the stack from aserver_name ${GRAFANA_DOMAIN}block and keeps adefault_serverthat answers everything else withreturn 444— a silent close. Socurl https://localhost/logingets an empty reply and%{http_code}prints000, which looks exactly like a dead Grafana while the stack is perfectly healthy. -
Probe through nginx on 443, not Grafana’s own port (see Accessing Web Interfaces). Grafana binds to
127.0.0.1:3100only, so the internal port answers on the host itself but never from another machine.
To separate “nginx is broken” from “Grafana is broken”, query Grafana directly on the host — 200 here with a failure above means the proxy is the problem:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3100/login
5. The dashboards actually have data:
cd /opt/grafana && bash misc/testing/check-dashboard-panels.sh --quiet
A target being UP is not the same as a dashboard working: a panel can query a metric series that simply does not exist on this host, and it then shows “No data” with nothing anywhere reporting a problem. This script runs every panel’s own query against Prometheus and reports which ones come back empty. The exit code is the number of dashboards with at least one unexpected empty panel, and credentials are read from .env automatically — no flags needed on a standard deployment.
Some empty panels are expected and not a sign of a broken install:
-
windows-exporter-iis-node— empty when there are no Windows hosts in this deployment -
oracle— empty on a MySQL deployment (and themysqldashboards empty on an Oracle one) -
api-metrics—api_method_requests_totalonly appears after real API traffic has occurred -
business-metrics— needs devices in the FTACS database -
a handful of panels in
nodes-monitoring/system-dashboard— Pressure Stall Information needs a recent kernel, power-supply panels need hardware sensors that may not exist on this host/VM
On the reference monitoring host, a run reports 13 empty panels out of 475 — every one explained by something that host doesn’t have, not by a defect. That count is not a target to match; it moves with what each host actually runs.
Troubleshooting
Quick Diagnostics: Run sudo bash /opt/grafana/misc/diagnose.sh to automatically check Docker, containers, ports, firewall, and Prometheus targets.
Common Issues and Solutions
Prometheus cannot connect to exporter
Symptoms: Target is DOWN in Prometheus UI, curl hangs
Step 1: Is exporter listening?
ss -ltpn | grep <port>
Step 2: Is container running?
docker ps
docker logs -f <CONTAINER_ID>
Step 3: Is exporter accessible from Prometheus?
docker exec $(docker ps | grep prom/prometheus | awk '{print $1}') \
wget http://<EXPORTER_IP>:<PORT>/metrics -O /tmp/test
If connection hangs — check firewall allows access from Prometheus host.
Cannot Login to Grafana
Symptom: Grafana login page loads but admin password is rejected (Invalid username or password)
Cause: Grafana stores the admin password in its SQLite database on first startup. Changing GRAFANA_ADMIN_PASSWORD in .env and restarting the container does not update the existing password — it only applies when creating a new admin user.
Solution: Reset password via CLI:
# On the Grafana server:
bash /opt/grafana/misc/bootstrap.sh --reset-password "new_password"
# Or directly:
docker exec grafana grafana cli admin reset-admin-password "new_password"
Spaces in .env Files
Wrong: MYSQL_USER = exporter (spaces around =)
Correct: MYSQL_USER=exporter (no spaces)
Forgetting to Create .env File
Symptom: Containers start but use default/empty values
Solution: Always run cp .env.example .env before docker compose up
Firewall Rules Not Persistent
Problem: Rules disappear after reboot
Solution: Our scripts auto-save rules. Manual save:
# Ubuntu/Debian
netfilter-persistent save
# CentOS/Rocky
service iptables save
Wrong Prometheus IP in Firewall
Symptom: Prometheus shows targets as “DOWN”
Solution: Verify Prometheus IP and update firewall rules.
Conflicting Container Names
Problem: “The container name is already in use”
Solution:
docker compose down
docker compose up -d
MySQL exporter shows mysql_up 0
Solution:
systemctl status mysql
mysql -u exporter -p -e "SHOW GRANTS;"
Nginx 502 Bad Gateway
Solution:
docker logs nginx_proxy
docker exec nginx_proxy env | grep GRAFANA_SERVICE_TOKEN
docker compose restart nginx
cAdvisor shows no container metrics (containerd image store)
Symptom: cAdvisor returns only root container (id="/"), no individual Docker containers visible. System Dashboard Basic shows no FT UI Services. cAdvisor logs show:
Failed to create existing container: failed to identify the read-write layer ID open /rootfs/var/lib/docker/image/overlayfs/layerdb/mounts/.../mount-id: no such file or directory
Cause: Storage Driver: overlayfs means Docker uses the containerd image store — the default for fresh installs of Docker Engine ≥ 29 (opt-in on 28, and on Debian 13 / RHEL 9 static-tarball installs). There is no /var/lib/docker/image//layerdb in that mode, and *cAdvisor before v0.54.0 cannot resolve a container’s read-write layer: it skips every container and reports only the root cgroup, so all container_* series for that host are lost.
Upstream fixed this in cAdvisor v0.54.0 (PR #3709, released 2025-12-02) — it reads the container rootfs from the containerd spec instead of the graphdriver metadata. This repo pins a newer build, so the fix is the cAdvisor version, not the Docker storage driver.
Two conditions, not one. Because the new code talks to containerd, cAdvisor’s docker factory needs the socket dockerd actually uses. With a wrong path the container starts, the read-write layer ID spam stops and everything looks fixed, but the log says Registration of the docker container factory failed: … dial unix /run/containerd/containerd.sock and every container_* series arrives without name/container_label_* — only raw cgroup ids (verified on grafrd: 13 named containers with a reachable socket, 0 without). cAdvisor’s default is /run/containerd/containerd.sock, correct for package installs; the offline static-tarball install (dockerd spawns its own containerd) keeps it under /run/docker/containerd/. Hence CONTAINERD_SOCK in exporters/base/.env, detected by detect_containerd_sock() (misc/lib/container-helpers.sh) on every quick-deploy run.
Check:
docker info | grep -E 'Storage Driver|driver-type' # overlayfs + io.containerd.snapshotter.v1
docker inspect --format='{{.Config.Image}}' cadvisor # must be >= v0.54.0
docker logs --since 10m cadvisor 2>&1 | grep -c 'read-write layer ID'
docker logs --since 10m cadvisor 2>&1 | grep 'docker container factory'
source /opt/grafana/misc/lib/container-helpers.sh && detect_containerd_sock
Solution — upgrade cAdvisor and pin the socket. No Docker restart, no downtime for other containers:
cd /opt/grafana/exporters/base
sed -i 's|^CADVISOR_VERSION=.*|CADVISOR_VERSION=v0.55.1|' .env # or add the line if missing
source /opt/grafana/misc/lib/container-helpers.sh
SOCK=$(detect_containerd_sock)
grep -q '^CONTAINERD_SOCK=' .env \
&& sed -i "s|^CONTAINERD_SOCK=.*|CONTAINERD_SOCK=$SOCK|" .env \
|| echo "CONTAINERD_SOCK=$SOCK" >> .env
docker compose up -d cadvisor
Air-gapped host — load the image first. A ready archive is published on the hub, together with a step-by-step page for customers: https://hub.friendly-tech.com/docs/pdf/grafana-cadvisor-fix.html
curl -fLO https://hub.friendly-tech.com/docs/pdf/cadvisor-v0.55.1.tar.gz
sha256sum cadvisor-v0.55.1.tar.gz
# expect 3439641369dea7121fc3ed1fb2722e979421b0bab3c574d8115fd12d977aa204
# transfer, then on the target host:
docker load -i cadvisor-v0.55.1.tar.gz
Or build it yourself on any machine with internet access:
docker pull gcr.io/cadvisor/cadvisor:v0.55.1
docker save gcr.io/cadvisor/cadvisor:v0.55.1 -o cadvisor-v0.55.1.tar
tar tf cadvisor-v0.55.1.tar | grep -m1 manifest.json # classic format, loads anywhere
Verify:
docker logs --since 3m cadvisor 2>&1 | grep -c 'read-write layer ID' # expect 0
curl -s 127.0.0.1:9183/metrics | grep -o 'name="[^"]*"' | sort -u | wc -l # expect your container count
Last resort — switch Docker back to the classic store. Only for hosts that must keep an old cAdvisor build. Graph drivers are deprecated in Docker 29, this restarts Docker, and images already written to the containerd store disappear from docker images afterwards (they are not deleted — reverting daemon.json brings them back):
# add both keys to /etc/docker/daemon.json, keeping any existing settings:
# {"features": {"containerd-snapshotter": false}, "storage-driver": "overlay2"}
systemctl restart docker
docker info | grep 'Storage Driver' # should show overlay2
# air-gapped: re-load images before starting anything
docker load -i docker-images-bundle.tar.gz
cd /opt/grafana/exporters/base && docker compose up -d
On a fresh Docker 29 install this may not take effect at all — the daemon keeps the containerd store while its state exists, and a real switch means wiping /var/lib/docker. Upgrade cAdvisor instead.
Note: Bootstrap detects the containerd image store on every run and compares it with the deployed cAdvisor version: with v0.54.0+ it just logs that container metrics are supported; with an older build it prints an
ACTION REQUIREDblock in the final summary.diagnose.shperforms the same check. The--fix-storage-driverflag performs the last-resort switch above (works in non-interactive runs too), and is no longer offered interactively.
Grafana alerts not sending emails
Solution:
docker logs grafana | grep -i smtp
# Test in UI: Alerting > Contact points > email > Test
Alertmanager Config UI: “Failed to update .env file”
The UI runs as uid 1000 while bootstrap.sh runs as root, so the files it edits
must be group-owned by the host docker group — the same group that lets it
restart containers. Re-running bootstrap.sh stack sets this up (DOCKER_GID in
.env, plus 0660 on .env and 0664 on the templates and
prometheus_rules.yml, which other containers still have to read).
Check what the container actually sees:
cd /opt/grafana/prometheus-grafana-stack
docker compose exec alertmanager-config-ui sh -c 'id; ls -l /app/.env'
# root:root 0644 -> the grant is missing; re-run bootstrap, or:
chgrp "$(getent group docker | cut -d: -f3)" .env && chmod 0660 .env
A related symptom is “Configuration saved” with nothing changing: the write
succeeded but the restart that applies it did not, because neither Alertmanager
nor snmp-notifier re-reads .env while running. The UI then names the service
that is still on the old settings. Applying it by hand:
docker compose up -d alertmanager snmp-notifier
If the UI keeps behaving like an older build after an upgrade, compare the image
against the source — it is built from this repo, not pulled (see
prometheus-grafana-stack/alertmanager-config-ui/README.md).
Operations
Updating Monitoring Stack
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- stack
Select “Keep current configuration” to preserve your .env settings.
For exporters:
cd /opt/grafana
sudo bash /opt/grafana/misc/quick-deploy.sh PROMETHEUS_IP
Just refresh the code, deploy nothing — --update pulls the requested branch into /opt/grafana and stops. It needs no PROMETHEUS_IP, installs nothing, and restarts no container, so it is safe on a server whose stack is running:
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- --update --branch 1.6.9
It prints the commit it moved from and to. Use it to pick up a fix and apply it deliberately afterwards (bootstrap.sh stack, quick-deploy.sh, or a single docker compose up -d).
Why not plain
git pull? The repository is private and servers are installed with an HTTPS remote and no stored credentials —git pullthere fails withcould not read Username for 'https://github.com'. The hub wrapper carries the token, hands it to bootstrap, and the token is stripped from the remote again right after the pull. An extracted offline copy has no history at all:--updatesays so instead of reporting success, and such a host is refreshed by delivering a new archive.
Updating Configuration
vi .env
docker compose up -d
docker compose restart nginx # if nginx config changed
Updating Docker Images
vi .env # Change version, e.g., PROMETHEUS_VERSION=v3.6.0
docker compose pull prometheus
docker compose up -d prometheus
Rollback Deployment
The deployment script creates a manifest tracking all changes. To rollback:
# Rollback to previous state (latest manifest)
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- --rollback
# Or directly:
sudo /opt/grafana/misc/quick-deploy.sh --rollback
# List available manifests
sudo /opt/grafana/misc/quick-deploy.sh --list-manifests
# Rollback specific deployment
sudo /opt/grafana/misc/quick-deploy.sh --rollback /opt/grafana/.deploy-manifests/manifest-20260201-143022
What gets rolled back:
-
Stops deployed containers
-
Removes added firewall rules
-
Restores original configuration files (WildFly, Hazelcast)
Note: Services like WildFly/ACS may need manual restart after rollback.
Offline/Air-gapped Installation
Docker must already be installed on the air-gapped server (
install-docker.shneeds internet).
Prepare (machine with internet) — use the 1.6.9 branch ZIP (not main), and build on a
host with the classic Docker image store (Docker Desktop / Engine ≥ 28 use the containerd
store → docker save writes OCI → old Docker on the target fails with unrecognized image format):
bash misc/download-docker-images.sh # linux/amd64 by default; includes built stack images + Grafana plugins
tar tf docker-images-bundle.tar* | grep -m1 manifest.json && echo CLASSIC_OK # NOT oci-layout
scp grafana.zip docker-images-bundle.tar.gz user@server:/tmp/
scp -r docker-images-bundle-grafana-plugins user@server:/tmp/
Install exporters (air-gapped server):
sudo docker load -i /tmp/docker-images-bundle.tar.gz
cd /opt && sudo unzip /tmp/grafana.zip && sudo mv grafana-* grafana
sudo bash /opt/grafana/misc/quick-deploy.sh PROMETHEUS_IP
Install monitoring stack (air-gapped server): load the bundle and the repository as above, then run bootstrap. Run it plain for the interactive configurator, or add -y to skip every prompt:
sudo docker load -i /tmp/docker-images-bundle.tar.gz
sudo bash /opt/grafana/misc/bootstrap.sh stack # interactive: prompts for domain, passwords, SMTP
sudo bash /opt/grafana/misc/bootstrap.sh stack -y # non-interactive: no questions; auto IP-as-domain + random passwords
With Docker and the repo already present, bootstrap runs fully offline — self-signed cert, htpasswd, alertmanager placeholder, containers from pre-loaded images, base exporters, targets — and auto-stages the bundled Grafana plugins if docker-images-bundle-grafana-plugins/ is next to /opt/grafana or in /tmp. No manual section-8 steps needed (see the Quick Start offline section for details and the Oracle datasource plugin).
Upgrading a stack: two images are built here, not pulled. snmp-catcher:local and alertmanager-config-ui:local are built from this repository. On an upgrade bootstrap rebuilds each one only if a file in its source directory is newer than the image, so a fix to their Python actually takes effect; if that build fails (no internet) it keeps the existing image and says so, since an air-gapped host already loaded a current image from the bundle. Everything else comes from a registry and is pinned in .env.
Security Best Practices
-
Never commit
.envfiles to git -
Use strong passwords (16+ characters)
-
Restrict permissions:
chmod 600 .env -
Rotate passwords every 90 days
-
Use dedicated users with minimal grants
Deployment Checklist
Before Starting:
-
Docker installed, repository cloned to /opt/grafana
-
Prometheus IP address known
-
Credentials gathered (MySQL/Oracle/SMTP)
For Each Server:
-
.envconfigured, containers deployed -
Metrics endpoints working (
curl localhost:PORT/metrics) -
Firewall rules saved
On Prometheus/Grafana:
-
Targets show “UP” in Prometheus UI
-
Dashboards loading, alerts configured
-
Nginx proxy working (/grafana-ro/)
For Developers
Documentation
This repository automatically maintains Antora-formatted documentation alongside the Markdown README.
Automatic Conversion
When you commit changes to README.md, a pre-commit hook automatically converts it to AsciiDoc format for Antora documentation. The converted file is placed in docs/modules/ROOT/pages/index.adoc and staged for the commit.
Setting Up Git Hooks
After cloning this repository, install the Git hooks:
./misc/install-hooks.sh
This installs a pre-commit hook that:
-
Detects when
README.mdis staged for commit -
Converts it to AsciiDoc using
pandoc -
Stages the converted
index.adocfile automatically
Requirements:
-
pandocmust be installed:
# macOS brew install pandoc # Ubuntu/Debian sudo apt-get install pandoc # CentOS/RHEL sudo yum install pandoc
Continuous Integration
Six workflows live in .github/workflows/:
-
Sandbox tests (
sandbox-tests.yml) — runs the installer test suite in a container on every push to a1.*branch and on every pull request. -
Generate PDF Documentation (
docs-pdf.yml) — rebuilds the PDF handouts. -
Trigger Docs Portal Rebuild (
trigger-docs-portal.yml) — notifies the portal repository whenREADME.md,docs/*orVERSIONchanges. Note the portal builds only from1.branches and tags, so a docs fix onmainchanges nothing. -
Build SNMP Catcher / Build Alertmanager Config UI — publish those two images to Harbor, tagged by branch.
latestis pushed only frommain. -
Fixture Drift (
fixture-drift.yml) — weekly comparison of the sandbox fixtures against the reference host, so the mocks cannot go on replaying a world that has moved. Two jobs:recordruns on a self-hosted runner on the reference host, because its ufw admits inbound ssh from an allowlist that hosted runners are not on, and a runner long-polls outbound instead;analyseruns hosted, because the secret scan and the sandbox suite need Docker and that runner’s account deliberately has none. It opens one rolling issue rather than committing, because a fixture change alters what every sandbox case believes about reality. Setup inmisc/testing/sandbox/README.md.
Two things about triggers are worth knowing before adding a workflow here.
schedule runs on the default branch only. GitHub takes the workflow from
the latest commit on main regardless of where it was developed, so a weekly
job that lives only on a version branch never fires. That is why
fixture-drift.yml is on main as well as on the development line.
workflow_dispatch is not limited to the default branch — it dispatches
against any ref, and the version that runs is the one on that ref. Measured
05.08.2026: gh workflow run fixture-drift.yml --ref 1.6.9 ran seven hours
before the file existed on main at all.
What does trip people up is registration, which is a different thing: GitHub
does not know a workflow exists until some event has caused it to read the
file. A workflow whose only triggers are schedule and workflow_dispatch
gives it no such event on a version branch, so the API reports it as
non-existent and there is nothing to dispatch. Push the file with any trigger
that fires once — a temporary push: is enough — and dispatch works from then
on, on any branch.
Development & Releases
The project uses version branches for development and releases.
Workflow:
1. Work on version branch (e.g., 1.6.5) 2. When ready: ./misc/release.sh [patch|minor|major] → Clears the prerelease marker in docs/antora.yml → Merges to main, tags, creates the GitHub Release with a changelog → DELETES the version branch, locally and on the remote → Creates the next version branch (e.g., 1.6.6) with the marker back 3. Continue development on new branch
Two steps in there surprise people, so they are spelled out rather than left in the script:
-
The version branch is deleted, both locally and on the remote. It has been merged and tagged by then, so nothing is lost — but anything still unpushed on it is, and a colleague tracking that branch will find it gone.
-
The
prerelease:marker is stripped before the merge. Every new development branch carriesprerelease: -devindocs/antora.yml, and nothing used to remove it, so a released line kept publishing itself to the docs portal asX.Y.Z-dev, sorted below an older clean version and never becoming latest. If a release ever shows up on the portal with a-devsuffix, this is the step that did not run.
Version branch naming:
-
Branches:
1.6.5(withoutvprefix) -
Tags:
1.6.5(withoutvprefix) -
Releases:
1.6.5(withoutvprefix)
Using specific versions:
# List available versions
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- --version-list
# Install specific version
curl -fsSL https://hub.friendly-tech.com/bootstrap/install.sh | sudo bash -s -- stack --branch 1.6.4