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:
- Requirements
- Server preparation
- Web server (Nginx)
- PHP 8.1 and ionCube
- PHP-FPM pool
- MySQL
- Application files
- Configuration (.env)
- Database
- Composer
- Sphinx
- Vue SSR service
- Cron manager
- Email sending
- SELinux and firewall
- HTTPS
- 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_childrenlimits how many PHP processes can run at the same time. Size it by available RAM (roughly 50–100 MB per process).open_basedirlocks PHP file access to the project directory.sendmail_pathsets the envelope sender for outgoing mail — use your domain in thenoreply@address.- The socket path must match
fastcgi_passin 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:
/custom/— modified files of extensions/files/— system files: cache, log files, database migration files/plugins/— plugins/public_html/custom/— modified static files/public_html/files/— public files (images, …)/public_html/plugins/— static files of plugins/public_html/themes/— static files of themes/themes/— themes
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, withoutwwwand without protocol.- The
MYSQL_*values must match the database created in the next step. CRYPT_KEYis 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_VERSIONmust match the installed Sphinx version (3.7.1 in this guide). The otherSPHINX_*andVUE_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
utf8mb4character set is required. mysql_native_passwordis required because the Sphinx indexer connects to the database with a client library that does not support the newercaching_sha2_passwordmethod. On MySQL 8.4 this requiresmysql_native_password = ONin 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
pcntlextension must be available in CLI mode (installed withphp-processearlier) — the cron manager uses it to run tasks in parallel. - Executed tasks are logged to
/files/logs/cron.loginside 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
adminand 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.