Environment Installation and Configuration

This guide describes a complete installation of the Platform on a clean server.

Target environment: Rocky Linux 10, Nginx + PHP-FPM. Follow the steps in order. All commands are run as root, unless a step says otherwise.

The following placeholder values are used in all examples. Replace them with your real values:

Placeholder Meaning
example.com site domain
/var/www/example project root directory
example Linux user that owns and runs the project
example_db MySQL database name
example_user MySQL user
db-password MySQL user password

Installation steps:

  1. Requirements
  2. Server preparation
  3. Web server (Nginx)
  4. PHP 8.1 and ionCube
  5. PHP-FPM pool
  6. MySQL
  7. Application files
  8. Configuration (.env)
  9. Database
  10. Composer
  11. Sphinx
  12. Vue SSR service
  13. Cron manager
  14. Email sending
  15. SELinux and firewall
  16. HTTPS
  17. Finishing the installation

Requirements

Before you start, you need:

  • The distribution archive of the platform (zip file with the latest version).
  • A clean server with Rocky Linux 10 and root access.
  • A domain with an A record pointing to the server IP address. The domain must already resolve — it is required for the HTTPS step.

Software stack installed in this guide:

Component Version Purpose
Nginx latest stable web server
PHP (FPM) 8.1 application runtime
ionCube Loader 15+ required to run the platform code
MySQL 8.0 / 8.4 database
Sphinx 3.7.1 search engine
Node.js 22 Vue server-side rendering (SSR)
Composer latest PHP dependencies
Postfix latest email sending

Required PHP extensions: curl, dom, fileinfo, gd, gettext, iconv, intl, json, libxml, mbstring, openssl, pcntl, pdo, pdo_mysql, posix, simplexml, spl, xmlreader, xmlwriter, zip, zlib.

Optional PHP extensions: imagick (better image thumbnails), memcached and apcu (cache drivers).

Required PHP setting: short_open_tag = On.

Server Preparation

Update the system and add the package repositories (EPEL, Remi, CRB):

dnf update -y
dnf install -y epel-release
dnf config-manager --set-enabled crb
dnf install -y https://rpms.remirepo.net/enterprise/remi-release-10.rpm
dnf install -y wget unzip tar policycoreutils-python-utils

Create the Linux user that will own and run the project:

groupadd example
useradd -m -d /var/www/example -g example -s /bin/bash example
chmod 755 /var/www/example

Do not run the project as root or as the nginx user. Each site must have its own user.

Web Server (Nginx)

dnf install -y nginx

Create /etc/nginx/conf.d/tuning.conf with settings that the platform needs (the default client_max_body_size of 1 MB is too small for file uploads):

client_max_body_size 50m;

gzip on;
gzip_static on;
gzip_min_length 1000;
gzip_comp_level 5;
gzip_proxied any;
gzip_vary on;
gzip_types text/plain text/css text/xml application/json application/javascript
           application/xml application/xml+rss image/svg+xml
           application/vnd.ms-fontobject font/ttf font/woff font/woff2;

Create the site configuration /etc/nginx/conf.d/example.com.conf. The archive ships a minimal example in /public_html/nginx.conf; the configuration below covers it and adds caching, security rules and the PHP handler:

server {
  listen 80;
  listen [::]:80;

  server_name example.com;
  root /var/www/example/public_html;
  access_log /var/log/nginx/example.com-access.log;
  error_log  /var/log/nginx/example.com-error.log error;

  index index.php;

  # application error pages
  error_page 401 /index.php?bff=errors&errno=401;
  error_page 403 /index.php?bff=errors&errno=403;
  error_page 404 /index.php?bff=errors&errno=404;
  error_page 500 /index.php?bff=errors&errno=500;
  error_page 501 /index.php?bff=errors&errno=501;
  error_page 502 /index.php?bff=errors&errno=502;
  error_page 504 /index.php?bff=errors&errno=504;

  location = /robots.txt { access_log off; log_not_found off; try_files $uri @rewrites; }

  # deny access to hidden and backup files
  location ~ /\. { deny all; access_log off; log_not_found off; }
  location ~ ~$  { deny all; access_log off; log_not_found off; }

  # static files
  location ~* \.(js|css|png|jpg|jpeg|gif|ico|swf|flv|eot|ttf|woff|woff2|pdf|xls|htc)$ {
    add_header Pragma "public";
    add_header Cache-Control "public, must-revalidate, proxy-revalidate";
    add_header Access-Control-Allow-Origin '*';
    access_log off;
    log_not_found off;
    expires 360d;
    try_files $uri @rewrites;
  }

  # never execute PHP from upload directories
  location ~* ^/(files|styles|css|img|rss|seo)/.*\.(php|php2|php3|php4|php5)$ {
    deny all;
  }

  if ($request_uri ~ " ") { return 404; }

  # certificate validation (used in the HTTPS step)
  location ^~ /.well-known/acme-challenge/ {
    allow all;
    default_type "text/plain";
  }

  # PHP handler
  location ~* \.php$ {
    try_files $uri =404;
    fastcgi_pass unix:/var/run/php-fpm/example.sock;
    fastcgi_index index.php;
    fastcgi_split_path_info ^(.+\.php)(.*)$;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param SCRIPT_NAME $fastcgi_script_name;
    fastcgi_param PATH_INFO $fastcgi_path_info;
    include fastcgi_params;
  }

  # zone for protected downloads (X-Accel-Redirect)
  location ^~ /files/partial/ {
    internal;
    alias /var/www/example/public_html/files/;
  }

  # front controller
  location / {
    try_files $uri $uri/ @rewrites;
  }
  location @rewrites {
    rewrite ^ /index.php last;
  }
}

# www -> non-www redirect
server {
  listen 80;
  listen [::]:80;

  server_name www.example.com;

  location ^~ /.well-known/acme-challenge/ {
    allow all;
    default_type "text/plain";
  }

  location / {
    return 301 http://example.com$request_uri;
  }
}

Check the configuration and start Nginx:

nginx -t
systemctl enable --now nginx

PHP 8.1 and ionCube

Install PHP 8.1 from the Remi repository:

dnf module reset -y php
dnf module enable -y php:remi-8.1
dnf install -y php php-fpm php-cli php-pdo php-mysqlnd php-gd php-mbstring \
               php-xml php-intl php-zip php-process php-opcache

If a different PHP version was already installed from the system repository, remove it first: dnf remove -y php php-common php-fpm, then repeat the commands above.

Verify the version and the required extensions:

php -v
for e in curl dom fileinfo gd gettext iconv intl json libxml mbstring openssl \
         pcntl pdo pdo_mysql posix simplexml spl xmlreader xmlwriter zip zlib; do
  php -r "exit(extension_loaded('$e') ? 0 : 1);" || echo "MISSING: $e"
done

The loop must print nothing. If an extension is missing, find and install its package (dnf provides 'php-*' helps).

Edit /etc/php.ini and set:

short_open_tag = On
memory_limit = 128M
post_max_size = 40M
upload_max_filesize = 40M
date.timezone = Europe/Berlin   ; set your timezone
display_errors = Off
log_errors = On

ionCube Loader

The platform code is protected with ionCube and does not run without the loader:

cd /tmp
wget https://downloads.ioncube.com/loader_downloads/ioncube_loaders_lin_x86-64.tar.gz
tar xzf ioncube_loaders_lin_x86-64.tar.gz
cp ioncube/ioncube_loader_lin_8.1.so /usr/lib64/php/modules/
restorecon /usr/lib64/php/modules/ioncube_loader_lin_8.1.so
echo 'zend_extension = /usr/lib64/php/modules/ioncube_loader_lin_8.1.so' > /etc/php.d/01-ioncube.ini
rm -rf /tmp/ioncube /tmp/ioncube_loaders_lin_x86-64.tar.gz

On an ARM server use ioncube_loaders_lin_aarch64.tar.gz instead.

Verify — the output must contain the line “with the ionCube PHP Loader”:

php -v

PHP-FPM Pool

The application runs in its own PHP-FPM pool. The pool process runs as the site user (example), so the application can write to its own files without extra permissions. Nginx talks to the pool through a unix socket.

Create /etc/php-fpm.d/example.conf:

[example]
user = example
group = nginx

listen = /var/run/php-fpm/example.sock
listen.owner = example
listen.group = nginx
listen.mode = 0660

pm = dynamic
pm.max_children = 25
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 25
pm.max_requests = 200

chdir = /

php_admin_value[open_basedir] = /var/www/example:/tmp/
php_admin_value[sendmail_path] = /usr/sbin/sendmail -t -i -f noreply@example.com
php_admin_value[upload_tmp_dir] = /var/www/example/tmp
php_admin_value[session.save_path] = /var/www/example/tmp
php_admin_value[display_errors] = stderr
php_admin_value[log_errors] = On

Notes:

  • pm.max_children limits how many PHP processes can run at the same time. Size it by available RAM (roughly 50–100 MB per process).
  • open_basedir locks PHP file access to the project directory.
  • sendmail_path sets the envelope sender for outgoing mail — use your domain in the noreply@ address.
  • The socket path must match fastcgi_pass in the Nginx site configuration.

Start PHP-FPM:

systemctl enable --now php-fpm

MySQL

dnf install -y mysql-server

Rocky Linux 10 provides MySQL 8.4. Any MySQL 8.0-compatible build also works.

Create /etc/my.cnf.d/zz-platform.cnf:

[mysqld]
# the platform does not support strict SQL mode
sql_mode =
max_allowed_packet = 128M
# needed for the Sphinx indexer to connect (MySQL 8.4 only, remove this line on 8.0)
mysql_native_password = ON

Start MySQL and set the root password:

systemctl enable --now mysqld
mysql_secure_installation

Verify that strict mode is off — the query must return an empty value:

mysql -u root -p -e "SELECT @@sql_mode;"

Application Files

Unpack the distribution archive into the project root so that bff.php is directly inside /var/www/example:

cd /var/www/example
unzip /path/to/platform_archive.zip

Set ownership and permissions:

chown -R example:example /var/www/example
find /var/www/example -type d -exec chmod 755 {} +
find /var/www/example -type f -exec chmod 644 {} +

mkdir -p /var/www/example/tmp
chown example:nginx /var/www/example/tmp
chmod 775 /var/www/example/tmp

The tmp directory is used for uploads and PHP sessions (see the pool configuration above).

Because the PHP-FPM pool runs as the owner of the files (example), no group or world write access is needed on the application directories. The directories the application writes to at runtime are:

On a server with SELinux enabled these directories also need SELinux labels — see SELinux and firewall.

Configuration (.env)

The main environment settings live in the .env file at the project root. The system configuration file /config/sys.php reads them, so in a standard installation you edit only .env.

cd /var/www/example
cp .env.example .env
chown example:example .env
chmod 600 .env

Generate a unique encryption key for this installation:

echo "CRYPT_KEY=base64:$(openssl rand -base64 32)"

Fill in .env (add lines that are missing in .env.example):

HOST_NAME=example.com
MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_DATABASE=example_db
MYSQL_USER=example_user
MYSQL_PASSWORD=db-password
CRYPT_KEY=base64:...   # the value generated above
SPHINX_ENABLED=1
SPHINX_HOST=127.0.0.1
SPHINX_PORT=9306
SPHINX_VERSION=3.7.1
VUE_SSR_HOST=127.0.0.1
VUE_SSR_PORT=13715
VUE_SSR_WORKERS=0
  • HOST_NAME — the project domain, without www and without protocol.
  • The MYSQL_* values must match the database created in the next step.
  • CRYPT_KEY is used to encrypt data in the database. Set it once, before the first start, and never change it later — changing it makes already encrypted data unreadable. Keep a copy of it together with your backups.
  • SPHINX_VERSION must match the installed Sphinx version (3.7.1 in this guide). The other SPHINX_* and VUE_SSR_* values match the services configured later in this guide; keep the defaults.

Settings that are not present in .env (site title, and others) are edited in /config/sys.php or in the admin panel. See configuration and database connection.

Database

Create the database and the user:

mysql -u root -p
CREATE DATABASE example_db CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE USER 'example_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'db-password';
GRANT ALL PRIVILEGES ON example_db.* TO 'example_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Notes:

  • The utf8mb4 character set is required.
  • mysql_native_password is required because the Sphinx indexer connects to the database with a client library that does not support the newer caching_sha2_password method. On MySQL 8.4 this requires mysql_native_password = ON in the server configuration (set in the MySQL step).

Import the initial data from the archive. The categories file is language-specific — import the one matching your site language (en or ru):

cd /var/www/example
mysql -u example_user -p example_db < install/install.sql
mysql -u example_user -p example_db < install/install.categories.en.sql
mysql -u example_user -p example_db < install/install.regions.sql

Composer

Install Composer:

cd /tmp
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php composer-setup.php --install-dir=/usr/local/bin --filename=composer
rm composer-setup.php

Install the external libraries. Composer runs in the /bff subdirectory of the project, as the site user (do not run it as root):

cd /var/www/example/bff
sudo -u example /usr/local/bin/composer install --no-dev

If Composer stops with a memory error, run it as sudo -u example php -d memory_limit=1024M /usr/local/bin/composer install --no-dev.

Sphinx

Sphinx provides the search on the site. This guide installs version 3.7.1. See also the detailed page about Sphinx configuration.

Install the binaries and create the sphinx user (it is added to the example group so that the indexer can write to the project’s index files):

groupadd sphinx
useradd -r -g sphinx -G example -d /var/lib/sphinx -s /sbin/nologin sphinx

cd /tmp
wget https://sphinxsearch.com/files/sphinx-3.7.1-da9f8a4-linux-amd64.tar.gz
tar xzf sphinx-3.7.1-da9f8a4-linux-amd64.tar.gz
cp sphinx-3.7.1/bin/* /usr/bin/

mkdir -p /etc/sphinx /var/lib/sphinx /var/log/sphinx
chown sphinx:sphinx /var/lib/sphinx /var/log/sphinx

mkdir -p /var/www/example/files/sphinx
chown example:example /var/www/example/files/sphinx
chmod 775 /var/www/example/files/sphinx

On an ARM server use the linux-aarch64 tarball.

Generate the project’s Sphinx configuration (creates /var/www/example/config/sphinx.conf; run it again whenever the database connection settings change):

cd /var/www/example/public_html
sudo -u example php index.php bff=sphinx

Create the system configuration. Copy the template shipped with the platform and point it at the project:

cp /var/www/example/config/sphinx.conf.global /etc/sphinx/sphinx.conf
chmod 755 /etc/sphinx/sphinx.conf

Edit /etc/sphinx/sphinx.conf — in the files=(...) block, keep one line with the real path to the project configuration:

files=(
"/var/www/example/config/sphinx.conf"
)

This file is a script: Sphinx executes it and reads its output as the configuration, which lets one Sphinx instance serve several projects.

Run the initial indexing (must finish without errors):

sudo -u sphinx indexer --config /etc/sphinx/sphinx.conf --all

Create the systemd service /etc/systemd/system/searchd.service:

[Unit]
Description=Sphinx search engine (searchd)
After=network.target

[Service]
Type=forking
PIDFile=/var/run/sphinx/searchd.pid
User=sphinx
Group=sphinx
RuntimeDirectory=sphinx
ExecStart=/usr/bin/searchd --config /etc/sphinx/sphinx.conf
Restart=always
RestartSec=30

[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now searchd

Set up index rotation — copy the script shipped with the platform and run it from cron every 5 minutes as the sphinx user:

cp /var/www/example/config/sphinx_rotate.sh /etc/sphinx/
chown sphinx:sphinx /etc/sphinx/sphinx_rotate.sh
chmod 755 /etc/sphinx/sphinx_rotate.sh
crontab -u sphinx -e
*/5 * * * * /etc/sphinx/sphinx_rotate.sh /var/www/example

Enable Sphinx as the search engine for listings:

cd /var/www/example/public_html
sudo -u example php index.php bff=config key=listings.search.engine value=sphinx

Vue SSR Service

The Platform renders some parts of its pages on the server with Vue (SSR). This requires Node.js 22 and a background service.

Install Node.js 22:

curl -fsSL https://rpm.nodesource.com/setup_22.x | bash -
dnf install -y nodejs

Install the JavaScript dependencies (as the site user, including dev dependencies — the SSR runner needs them):

cd /var/www/example
sudo -u example npm install

Create the systemd service /etc/systemd/system/bffssr.service:

[Unit]
Description=BFF Vue SSR
After=network.target

[Service]
Type=forking
ExecStart=/usr/bin/npm run ssr
WorkingDirectory=/var/www/example
User=example
Group=example
StandardInput=null
StandardOutput=journal
StandardError=journal
Restart=always

[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now bffssr

Verify that the service listens on the port set in .env (VUE_SSR_PORT, default 13715):

ss -tlnp | grep 13715

Cron Manager

The cron manager runs all background tasks of the application. Configure it to run every minute as the site user (never as root):

dnf install -y cronie
systemctl enable --now crond
crontab -u example -e
* * * * * /usr/bin/php -q /var/www/example/public_html/index.php bff=cron-manager

Notes:

  • The pcntl extension must be available in CLI mode (installed with php-process earlier) — the cron manager uses it to run tasks in parallel.
  • Executed tasks are logged to /files/logs/cron.log inside the project.

Email Sending

The application sends mail through the local sendmail binary. Postfix provides it:

dnf install -y postfix
systemctl enable --now postfix

Set the addresses used by the application:

cd /var/www/example/public_html
sudo -u example php index.php bff=config key=mail.noreply value=noreply@example.com
sudo -u example php index.php bff=config key=mail.admin value=admin@example.com

For mail to reach inboxes (and not spam folders), create these DNS records for the domain:

  • SPF — TXT record: v=spf1 a mx ~all
  • DKIM — sign outgoing mail with OpenDKIM and publish the public key as a TXT record
  • DMARC — TXT record _dmarc.example.com: v=DMARC1; p=none;
  • PTR — ask your hosting provider to set the reverse DNS of the server IP to the server hostname

Setting up OpenDKIM is outside the scope of this guide, but it is strongly recommended for production sites.

SELinux and Firewall

SELinux

Check the mode:

getenforce

If the result is Enforcing, allow the web application to write to its runtime directories and to open network connections (PHP connects to Sphinx and to the SSR service) and send mail:

for d in files tmp config custom plugins themes bff/vendor \
         public_html/files public_html/custom public_html/plugins public_html/themes; do
  semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/example/$d(/.*)?"
done
restorecon -R /var/www/example

setsebool -P httpd_can_sendmail on
setsebool -P httpd_can_network_connect on

Firewall

Open the web and mail ports:

firewall-cmd --permanent --add-service=http --add-service=https --add-service=smtp
firewall-cmd --reload

HTTPS

Issue a free Let’s Encrypt certificate with certbot. The domain (and www subdomain) must already point to this server, and the HTTP site from the Nginx step must be reachable — certbot validates the domain through the /.well-known/acme-challenge/ location.

dnf install -y certbot
certbot certonly -m admin@example.com --agree-tos --webroot \
  -w /var/www/example/public_html \
  -d example.com -d www.example.com \
  --deploy-hook "systemctl reload nginx"

Enable automatic renewal:

systemctl enable --now certbot-renew.timer

Update /etc/nginx/conf.d/example.com.conf. The port 80 servers now only redirect to HTTPS, and the site configuration moves into a port 443 server:

server {
  listen 80;
  listen [::]:80;

  server_name example.com www.example.com;
  root /var/www/example/public_html;

  location ^~ /.well-known/acme-challenge/ {
    allow all;
    default_type "text/plain";
  }

  location / {
    return 301 https://example.com$request_uri;
  }
}

# www -> non-www redirect
server {
  listen 443 ssl;
  listen [::]:443 ssl;
  http2 on;

  server_name www.example.com;

  ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
  ssl_protocols TLSv1.2 TLSv1.3;

  location / {
    return 301 https://example.com$request_uri;
  }
}

server {
  listen 443 ssl;
  listen [::]:443 ssl;
  http2 on;

  ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
  ssl_protocols TLSv1.2 TLSv1.3;
  add_header Strict-Transport-Security 'max-age=31536000';

  # ... the whole site configuration from the HTTP example goes here:
  # server_name, root, logs, index, error_page, all location blocks
}
nginx -t && systemctl reload nginx

Switch the application to HTTPS-only mode:

cd /var/www/example/public_html
sudo -u example php index.php bff=config key=https.only value=1
sudo -u example php index.php bff=config key=https.redirect value=1

The same options are available in the admin panel under Site settings.

If you have a certificate from another provider instead, place the certificate chain and the key on the server and use their paths in the ssl_certificate / ssl_certificate_key lines; the rest of the configuration is the same.

Finishing the Installation

Check that all services are running:

systemctl --no-pager status nginx php-fpm mysqld searchd bffssr crond postfix

Open https://example.com — the site must load. Then open the admin panel: https://example.com/admin/.

The administrator login is admin. To set the administrator password:

  • Create the file /files/admin_password_recovery.txt (inside the project root) containing the new administrator password.
  • Log in to the admin panel with the login admin and the password from the file.
  • After a successful login the password is saved and the file is deleted automatically.

The administrator is the user in the “Super Administrator” group (user ID #1). The same procedure restores access if the password is ever lost.

System Status

To verify the server and environment configuration, go to the “Site Settings / System Status” section of the admin panel and click “Check Again”. Fix any errors it reports — while critical errors are present, platform updates cannot be installed.