Running Cacti with Spine, PHP 7.4, and nginx on CentOS 7

Why Cacti + Spine

We use Cacti to monitor the devices in our own backbone and IDC footprint. What we value is its RRDtool-based storage and graphing, plus the mature tooling around it: remote and local data collection, device scanning and discovery, automated device and graph creation, custom collection methods, and user/group/domain access control. On a network that runs into the thousands of devices, those features earn their keep.

Cacti started as a high-school research project by Ian Berry around 2000, and it’s now a common choice for companies and network engineers monitoring enterprise networks and data centers.

The default PHP poller (cmd.php) struggles once you have a lot of devices, so we pair it with Spine, a multi-threaded collector written in C that connects to the database directly and polls concurrently, finishing thousands of devices inside a single poller cycle. This post records the full setup we run on CentOS 7: Cacti + Spine + PHP 7.4 + MySQL 5.7 + nginx.

One thing up front: the hard part here isn’t the installation, it’s a few configuration details that line up across components. The MySQL socket path, the charset, innodb_file_format, and Remi’s non-standard PHP paths are the places that bite. We’ll flag each as we go.

Base dependencies and database

1. Base dependencies

Set up the yum repos first, so later rpm dependencies can be found:

yum install epel-release.noarch -y
yum install https://rpms.remirepo.net/enterprise/remi-release-7.rpm -y

We use Remi’s packaged PHP rpms. Remi gives you a full range of PHP versions; the cost is that it puts the working paths, config paths, and session directory under remi subdirectories inside /etc/, /opt/, and /var/, not in the standard locations. Later, when we set Cacti’s PHP binary path and the session directory permissions, we have to accommodate that, or cmd.php / poller.php won’t run. That’s the first trap; note it now.

Then the base libraries:

yum install wget gcc gcc-c++ zlib-devel wget libcurl-devel openssl-devel bzip2-devel gmp-devel.x86_64 mpfr-devel.x86_64 libmpc-devel.x86_64 libSM-devel.x86_64 libxml2-devel gcc-gfortran.x86_64 bzip2-devel libjpeg-turbo-devel.x86_64 openjpeg-devel.x86_64 libpng12-devel.x86_64 libtiff-devel cmake3.x86_64 autoconf automake libtool yum-utils libSM-devel.x86_64 bzip2-devel libffi-devel jemalloc-devel.x86_64 gperftools-devel.x86_64 jasper-devel.x86_64 libxml2-devel tbb-devel libdc1394-devel gstreamer-devel.x86_64 blosc-devel libaec-devel brotli-devel.x86_64 giflib-devel.x86_64 gtk2-devel libaio-devel libsqlite3x-devel.x86_64 gperftools-devel.x86_64 make eigen3-devel ntp ntpdate perl-devel perl-CPAN perl-YAML fontconfig ttmkfdir -y

This pulls in a lot of image and even video libraries. If that feels excessive, you can skip the gstreamer-related ones, but keep the time-related packages (ntp/ntpdate) and the font-related ones (fontconfig/ttmkfdir): Cacti’s graphing depends on fonts, and collection and RRD are sensitive to time.

2. Install the database

We use MySQL 5.7:

wget http://repo.mysql.com/mysql57-community-release-el7-10.noarch.rpm
rpm -Uvh mysql57-community-release-el7-10.noarch.rpm
yum install  -y  mysql-community-server
rpm --import https://repo.mysql.com/RPM-GPG-KEY-mysql-2022

Don’t start it yet. Edit my.cnf to the following first, then start:

[mysql]
socket=/var/lib/mysql/mysql.sock
[mysqld]
symbolic-links=0
collation-server=utf8mb4_unicode_ci
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
default-time_zone = '+8:00'
user = mysql
bind-address = 127.0.0.1
skip_name_resolve = 1
innodb_file_per_table=1
innodb_file_format=Barracuda
lower_case_table_names=0
max_connect_errors = 2000
max_allowed_packet = 1024M
interactive_timeout = 1200
read_buffer_size = 16M
join_buffer_size = 128M
sort_buffer_size = 2M
read_rnd_buffer_size = 8M
sort_buffer_size = 8M
table_open_cache = 8
slow_query_log = 1
long_query_time = 6
expire_logs_days = 90
thread_stack = 256K
max_connections = 2000
wait_timeout=3600
net_read_timeout=3600
net_write_timeout=3600
interactive_timeout=3600
connect_timeout=28800

Three of these are things Cacti/Spine specifically care about, not generic tuning you can skim past:

  • socket=/var/lib/mysql/mysql.sock: Spine connects to the database over the socket by default, so if the socket path doesn’t match, Spine can’t connect.
  • collation-server=utf8mb4_unicode_ci: this drives Cacti’s encoding. Under utf8mb4 some tables hit the index key-length limit, and the wrong charset/collation makes a few tables fail to create when you import cacti.sql.
  • innodb_file_per_table=1 and innodb_file_format=Barracuda: these affect database performance and row format, and Cacti has row-format requirements for its larger tables.

On the first start, MySQL generates a temporary root password, which you’ll find in /var/log/message. Change it after the first login:

ALTER USER 'root'@'%' IDENTIFIED BY 'your_new_root_password';

Then load time-zone data into MySQL (Cacti needs the time-zone tables to handle time correctly):

mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root -p mysql

3. Install SNMP / RRDtool

Cacti needs fping, rrdtool, and net-snmp to work:

yum install net-snmp.x86_64 net-snmp-utils rrdtool-devel rrdtool.x86_64

4. Initialize the Cacti database

After logging into the database, create the database and user and import Cacti’s initial schema:

create database cacti DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
GRANT ALL PRIVILEGES ON `cacti` . * TO 'cactiuser'@'localhost' IDENTIFIED BY 'your_cacti_password';
GRANT SELECT ON mysql.time_zone_name TO 'cactiuser'@'localhost';
FLUSH PRIVILEGES;
use cacti
source /your/cacti/path/cacti.sql

Note the GRANT SELECT ON mysql.time_zone_name: Cacti has to be able to read the time-zone table to convert time correctly, and skipping this leaves graph timestamps wrong.

PHP and nginx

1. Install PHP

yum install php74.x86_64 php74-php-ffi php74-php-devel php74-php-cli php74-php-snmp php74-php-mysql php74-php-pdo php74-php-mysqlnd php74-php-bcmath.x86_64 php74-php-xml.x86_64 php74-php-gd.x86_64 php74-php-mbstring php74-php-process php74-php-ldap php74-php-gmp php74-php-intl

Remi’s PHP covers a full range of versions, but as noted, its working paths, config paths, and session directory all live under remi subdirectories, so you have to work around its environment and paths.

2. Configure php.ini and php-fpm

After the Remi install, the config files live under /etc/opt/remi/php<version>/. The values to set:

date.timezone =
cgi.fix_pathinfo=
memory_limit =

Configure php-fpm like this:

user = nobody
group = nobody
listen = /var/run/php7.sock
listen.owner = nobody
listen.group = nobody
listen.mode = 0660

Here we run PHP as the nobody user and group. That creates another Remi-path trap: the session directory Remi lays down (php_value[session.save_path] = /var/opt/remi/php74/lib/php/session), i.e. /var/opt/remi/php74/lib/php/session, isn’t owned by nobody by default. You must chown it to nobody:nobody and set permissions, or sessions can’t be written and logins won’t stick.

3. Install and configure nginx

yum install nginx -y

Main config:

user nobody;
worker_processes auto;
worker_rlimit_nofile 65535;
error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;

# Load dynamic modules. See /usr/share/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;

events {
    worker_connections 65535;
    use epoll;
    multi_accept on;
    accept_mutex on;
}
http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    error_log /var/log/nginx/error.log;

    include             mime.types;
    default_type        application/octet-stream;
    client_max_body_size 10000M;
    keepalive_timeout  300s 300s;
    keepalive_requests 10000;
    server_names_hash_bucket_size 51200;
    client_header_buffer_size 64M;
    large_client_header_buffers 4 64k;
    sendfile on;
    autoindex off;
    tcp_nopush on;
    tcp_nodelay on;
    gzip on;
    gzip_min_length 1k;
    gzip_buffers 4 16k;
    gzip_http_version 1.0;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml;
    gzip_vary on;
    gzip_disable     "MSIE [1-6]\.";
    server_tokens off;
    access_log /var/log/nginx/access.log;
    proxy_buffering on;
    map $http_upgrade $connection_upgrade {
           default upgrade;
           ''      close;
    }
    proxy_max_temp_file_size 0;
    proxy_cache_use_stale updating;
    proxy_cache_key $host$uri$is_args$args;
    proxy_cache_valid 200 301 1h;
    proxy_cache_valid any 10m;
    proxy_cache_path /tmp/cache levels=1:2 keys_zone=cache:512m max_size=512m inactive=16800h use_temp_path=off;
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/vhost/*.conf;
}

Create /etc/nginx/vhost/ and put cacti.conf in it:

server {
    listen 8443;          # Cacti port
    server_name _;        # Cacti domain
    index index.php index.htm index.html;
    root /home/www/html/cacti;   # Cacti working directory
    charset utf-8;
    location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.project|LICENSE|README.md)
    {
        deny all;
    }
    location ~ \.php$ {
                try_files $uri /index.php =404;
                fastcgi_split_path_info ^(.+\.php)(/.+)$;
                fastcgi_pass unix:/var/run/php7.sock;
                fastcgi_index index.php;
                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                include fastcgi_params;
        }
}

The fastcgi_pass target unix:/var/run/php7.sock has to match the listen in php-fpm. Once configured, remember to give nginx write access to /var/lib/nginx.

Install and configure Spine

1. Install Spine

wget https://www.cacti.net/downloads/spine/cacti-spine-1.y.z.tar.gz
tar -zxvf cacti-spine-1.y.z.tar.gz
cd cacti-spine-1.y.z
./bootstrap
./configure
make &  make install

Spine’s major version has to match Cacti’s, so replace 1.y.z with the version that matches your Cacti.

2. Spine config file

DB_Host       localhost
DB_Database   cacti
DB_User       <db user>
DB_Pass       <db password>
DB_Port       3306
#DB_UseSSL    0
#DB_SSL_Key
#DB_SSL_Cert
#DB_SSL_CA
Cacti_Log     /home/www/html/cacti/log/cacti.log   # match your Cacti working directory

DB_Host localhost works together with the MySQL socket setting earlier: Spine connects over the socket, which is exactly why we pinned socket=/var/lib/mysql/mysql.sock up front.

Start and configure Cacti

Extract Cacti to its install directory. The install wizard connects to the database, and at that point you need the PHP binary path set correctly, since this is Remi’s PHP and the path isn’t in a standard location:

INSERT INTO settings (name, value) VALUES ('path_php_binary', '/opt/remi/php74/root/bin/php');

If you didn’t install PHP via Remi, change it to your actual path. Also make sure Cacti has write access to the log and rra directories under its working directory, or it can’t produce graphs.

Then set up the poller crontab:

*/1 * * * * <absolute php binary path> /<cacti working path>/poller.php > /dev/null 2>&1

One more easily missed detail: cmd.php / poller.php start with

#!/usr/bin/env php

which relies on the PATH environment variable to find the php binary. Remi’s php isn’t on the default PATH, so either edit the shebang by hand, edit /etc/profile, or set it temporarily:

export PATH=$PATH:/opt/remi/php74/root/bin/

After that, open Cacti’s web port and configure your graphs.

Takeaway

None of this is hard to install; the hard part is lining up three kinds of paths and config: the MySQL socket has to match Spine, the charset and innodb_file_format have to satisfy Cacti’s schema, and Remi’s non-standard PHP path has to agree across the php-fpm run user, the session directory permissions, path_php_binary, and the poller’s PATH. Get those right and the rest follows the steps above.

Published At (Updated At) Tagged with