How to Install Chamilo with LEMP Stack on Rocky Linux 8

Install Chamilo with LEMP Stack on Rocky Linux 8 using PHP 8.3, MariaDB, Nginx, SELinux-safe permissions, HTTPS, and Chamilo 1.11.40 for a secure portal.
A glowing e-learning portal sits beside a Rocky Linux server rack and MariaDB database in a dark blue data center.

This guide shows you how to install Chamilo with LEMP Stack on Rocky Linux 8 and finish with a secured HTTPS learning portal. You will deploy Chamilo 1.11.40 with Nginx, MariaDB, PHP 8.3, and SELinux-aware permissions in about 15–20 minutes, assuming DNS already points to the VPS.

Prerequisites

  • Rocky Linux 8.10 or a fully updated Rocky Linux 8 server, with 2 vCPUs, 2 GB RAM, and at least 10 GB of free disk space.
  • Root access or a sudo-enabled account connected over SSH. The commands below assume a non-root user and use sudo.
  • A public IPv4 address and a domain such as YOUR_DOMAIN, with an A record already pointed at the server.
  • Ports 22, 80, and 443 allowed in both the VPS firewall and the server’s firewalld policy.
  • A long database password, a separate Chamilo administrator password, and an email address for certificate notices.
  • A VPSLake Linux VPS with enough memory for your expected number of learners and uploaded course files.

Step 1: Install the Rocky Linux web stack

Rocky Linux 8’s default PHP stream is not the right target for the 1.11 branch, so enable PHP 8.3 from Remi before installing PHP-FPM and its extensions.

sudo dnf update -y
sudo dnf install -y epel-release firewalld mariadb-server nginx unzip curl bind-utils policycoreutils-python-utils
sudo dnf install -y https://rpms.remirepo.net/enterprise/remi-release-8.rpm
sudo dnf module reset php -y
sudo dnf module enable php:remi-8.3 -y
sudo dnf install -y php php-cli php-fpm php-curl php-gd php-intl php-mbstring php-mysqlnd php-opcache php-process php-soap php-xml php-zip
sudo sed -ri 's/^[; ]*user = .*/user = nginx/; s/^[; ]*group = .*/group = nginx/; s/^[; ]*listen.owner = .*/listen.owner = nginx/; s/^[; ]*listen.group = .*/listen.group = nginx/' /etc/php-fpm.d/www.conf
sudo sed -ri 's/^memory_limit = .*/memory_limit = 256M/; s/^upload_max_filesize = .*/upload_max_filesize = 100M/; s/^post_max_size = .*/post_max_size = 100M/; s/^max_execution_time = .*/max_execution_time = 300/; s/^max_input_time = .*/max_input_time = 600/; s/^max_input_vars = .*/max_input_vars = 3000/' /etc/php.ini
sudo systemctl enable --now firewalld mariadb nginx php-fpm
sudo systemctl restart php-fpm
php -v
nginx -v
mariadb --version

You should see PHP 8.3.x, an Nginx version, and a MariaDB client version. Package patch numbers will change as Rocky Linux and Remi publish updates.

PHP 8.3.x (cli) ...
nginx version: nginx/...
mariadb  Ver ...

Step 2: Create the Chamilo database

Chamilo keeps accounts, courses, and portal settings in MariaDB. A local database account restricted to one schema keeps that data separate from other applications on the VPS.

First run the MariaDB hardening wizard and remove anonymous users, the test database, and remote root login when prompted:

sudo mysql_secure_installation

Create the database after replacing YOUR_DB_PASSWORD with a unique password. Do not reuse your SSH or Chamilo administrator password.

sudo mariadb <<'SQL'
CREATE DATABASE chamilo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'chamilo_user'@'localhost' IDENTIFIED BY 'YOUR_DB_PASSWORD';
GRANT ALL PRIVILEGES ON chamilo.* TO 'chamilo_user'@'localhost';
FLUSH PRIVILEGES;
SQL
sudo mariadb -e "SHOW DATABASES LIKE 'chamilo';"

The check should return a row named chamilo. The database does not need a public port because PHP-FPM will connect through the local socket or loopback interface.

Step 3: Download Chamilo and label its writable directories

Use the numbered archive listed on the official Chamilo download page instead of an unversioned development checkout. Chamilo 1.11.40 supports PHP 8.3 and is documented in the official 1.11 installation guide.

The extraction check below finds the archive’s top-level directory, so it remains safe if the ZIP layout changes slightly:

CHAMILO_VERSION=1.11.40
sudo curl -fL -o "/tmp/chamilo-${CHAMILO_VERSION}.zip" "https://github.com/chamilo/chamilo-lms/releases/download/v${CHAMILO_VERSION}/chamilo-${CHAMILO_VERSION}.zip"
sudo mkdir -p "/tmp/chamilo-${CHAMILO_VERSION}-extract" /var/www/chamilo
sudo unzip -q "/tmp/chamilo-${CHAMILO_VERSION}.zip" -d "/tmp/chamilo-${CHAMILO_VERSION}-extract"
CHAMILO_SOURCE="$(find "/tmp/chamilo-${CHAMILO_VERSION}-extract" -type f -path '*/main/install/index.php' -printf '%h\n' | sed 's#/main/install$##' | head -n 1)"
test -n "$CHAMILO_SOURCE"
sudo cp -a "$CHAMILO_SOURCE"/. /var/www/chamilo/

Nginx needs to read the application, while only the directories used for configuration and uploads should be writable. The SELinux rules make that permission survive a relabel or reboot.

sudo chown -R root:nginx /var/www/chamilo
sudo find /var/www/chamilo -type d -exec chmod 0750 {} \;
sudo find /var/www/chamilo -type f -exec chmod 0640 {} \;
sudo chown -R nginx:nginx /var/www/chamilo/app /var/www/chamilo/web /var/www/chamilo/main/default_course_document/images /var/www/chamilo/main/lang
sudo find /var/www/chamilo/app /var/www/chamilo/web /var/www/chamilo/main/default_course_document/images /var/www/chamilo/main/lang -type d -exec chmod 0770 {} \;
sudo find /var/www/chamilo/app /var/www/chamilo/web /var/www/chamilo/main/default_course_document/images /var/www/chamilo/main/lang -type f -exec chmod 0660 {} \;
sudo semanage fcontext -a -t httpd_sys_content_t '/var/www/chamilo(/.*)?'
sudo semanage fcontext -a -t httpd_sys_rw_content_t '/var/www/chamilo/(app|web|main/default_course_document/images|main/lang)(/.*)?'
sudo restorecon -Rv /var/www/chamilo

Step 4: Configure the Chamilo Nginx server block

Chamilo’s course, SCORM, document, and badge URLs need explicit rewrites; a basic try_files rule alone often produces 404 errors after login. Create the HTTP server block below and replace YOUR_DOMAIN in the server_name line.

sudo tee /etc/nginx/conf.d/chamilo.conf > /dev/null <<'NGINX'
server {
    listen 80;
    listen [::]:80;
    server_name YOUR_DOMAIN;

    root /var/www/chamilo;
    index index.php;
    charset utf-8;
    client_max_body_size 100m;
    autoindex off;

    location @chamilo_routes {
        rewrite ^/certificates/$ /certificates/index.php last;
        rewrite ^/courses/([^/]+)/$ /main/course_home/course_home.php?cDir=$1 last;
        rewrite ^/courses/([^/]+)/index.php$ /main/course_home/course_home.php?cDir=$1 last;
        rewrite ^/courses/([^/]+)/scorm/(.*\.(js|css|png|jpg|jpeg|gif))$ /app/courses/$1/scorm/$2 last;
        rewrite ^/courses/([^/]+)/scorm/(.*)$ /main/document/download_scorm.php?doc_url=/$2&cDir=$1 last;
        rewrite ^/courses/([^/]+)/document/certificates/(.*)$ /app/courses/$1/document/certificates/$2 last;
        rewrite ^/courses/([^/]+)/document/(.*)$ /main/document/download.php?doc_url=/$2&cDir=$1 last;
        rewrite ^/courses/([^/]+)/upload/([^/]+)/(.*)$ /main/document/download_uploaded_files.php?code=$1&type=$2&file=$3 last;
        rewrite ^/courses/([^/]+)/work/(.*)$ /main/work/download.php?file=work/$2&cDir=$1 last;
        rewrite ^/courses/([^/]+)/upload/course_home_icons/(.*\.(png|jpg|jpeg|gif))$ /app/courses/$1/upload/course_home_icons/$2 last;
        rewrite ^/courses/([^/]+)/(.*)$ /app/courses/$1/$2 last;
        rewrite ^/session/(\d+)/about/?$ /main/session/about.php?session_id=$1 last;
        rewrite ^/course/(\d+)/about/?$ /main/course_info/about.php?course_id=$1 last;
        rewrite ^/badge/(\d+) /main/badge/issued.php?issue=$1 last;
        rewrite ^/skill/(\d+)/user/(\d+) /main/badge/issued_all.php?skill=$1&user=$2 last;
        rewrite ^/badge/(\d+)/user/(\d+) /main/badge/issued_all.php?skill=$1&user=$2 last;
        rewrite ^/main/exercice/(.*)$ /main/exercise/$1 last;
        rewrite ^/main/newscorm/(.*)$ /main/lp/$1 last;
        rewrite ^/service/(\d+)$ /plugin/buycourses/src/service_information.php?service_id=$1 last;
        rewrite ^/main/upload/users/(.*)/(.*)/my_files/(.*)$ /app/upload/users/$1/$2/my_files/$3 last;
        try_files $uri /index.php$is_args$args;
    }

    location / {
        try_files $uri @chamilo_routes;
    }

    location /main {
        rewrite ^/main/([^/]+)/?$ /main/$1/index.php last;
    }

    location ~ \.php$ {
        try_files $uri @chamilo_routes;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param HTTPS $https;
        fastcgi_pass unix:/run/php-fpm/www.sock;
    }

    location ~* \.(png|jpe?g|gif|ico|js|css|mp3|swf|flv|mp4|ogg|woff|woff2)$ {
        rewrite ^/courses/([^/]+)/course-pic85x85.png$ /app/courses/$1/course-pic85x85.png last;
        rewrite ^/courses/([^/]+)/course-pic.png$ /app/courses/$1/course-pic.png last;
        rewrite ^/courses/([^/]+)/scorm/(.*)$ /main/document/download_scorm.php?doc_url=/$2&cDir=$1 last;
        rewrite ^/courses/([^/]+)/document/(.*)$ /main/document/download.php?doc_url=/$2&cDir=$1 last;
        rewrite ^/courses/([^/]+)/work/(.*)$ /main/work/download.php?file=work/$2&cDir=$1 last;
        rewrite ^/courses/([^/]+)/upload/(.*)$ /app/courses/$1/upload/$2 last;
        rewrite ^/courses/(.*)$ /app/courses/$1 break;
        expires 30d;
        access_log off;
        try_files $uri @chamilo_routes;
    }

    location ^~ /app/config/ { deny all; }
    location ^~ /vendor/ { deny all; }
    location ^~ /tests/ { deny all; }
    location ^~ /main/inc/lib/javascript/bigupload/files/ { deny all; }
    location ~ /\. { deny all; }
    location ~* /web\.config$ { deny all; }
}
NGINX
sudo nginx -t
sudo systemctl reload nginx

Nginx should print syntax is ok and test is successful. Keep the document root at /var/www/chamilo; the rewrite targets assume that exact path.

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Step 5: Open the firewall and enable HTTPS

Allow only the services needed for administration, certificate validation, and normal web traffic. Certbot can then add the TLS listener and redirect HTTP requests without exposing MariaDB.

sudo firewall-cmd --permanent --zone=public --add-service=ssh
sudo firewall-cmd --permanent --zone=public --add-service=http
sudo firewall-cmd --permanent --zone=public --add-service=https
sudo firewall-cmd --reload
sudo firewall-cmd --list-services
sudo dnf install -y certbot python3-certbot-nginx
sudo certbot --nginx --redirect --agree-tos --no-eff-email -m YOUR_EMAIL -d YOUR_DOMAIN
sudo nginx -t

The service list should contain ssh, http, and https. The certificate request will fail unless YOUR_DOMAIN resolves publicly to this VPS and port 80 is reachable.

Step 6: Run the Chamilo browser installer

The web installer writes the portal configuration and creates the first administrator, so this is where the database values become part of the application.

Open https://YOUR_DOMAIN/main/install/index.php and enter these values when asked:

  • Database type: MySQL or MariaDB
  • Database host: localhost
  • Database port: 3306
  • Database name: chamilo
  • Database user: chamilo_user
  • Database password: the value used for YOUR_DB_PASSWORD

Choose the portal language, set a unique administrator password, and finish the wizard. If it reports a write error, fix ownership and SELinux labels from Step 3 before continuing.

After the dashboard loads, make the generated configuration readable but no longer writable by the web process:

sudo chown -R root:nginx /var/www/chamilo/app/config
sudo find /var/www/chamilo/app/config -type d -exec chmod 0750 {} \;
sudo find /var/www/chamilo/app/config -type f -exec chmod 0640 {} \;
sudo rm -f /tmp/chamilo-1.11.40.zip

Verify the Chamilo installation

Check the services and required PHP modules from SSH, then sign in over HTTPS and create a small course with one uploaded document. That last browser test exercises the rewrites, database connection, TLS, and writable paths together.

sudo systemctl is-active nginx php-fpm mariadb firewalld
sudo nginx -t
php -v | head -n 1
php -m | grep -E 'curl|gd|intl|mbstring|mysqli|PDO|soap|xml|zip'
curl -fsSI https://YOUR_DOMAIN/
active
active
active
active
syntax is ok
test is successful
PHP 8.3.x (cli) ...
HTTP/2 200

The module order can differ, and a redirect status is also normal if the portal sends unauthenticated visitors to its login page. A successful login and course-file upload are the meaningful application checks.

Troubleshooting

No matching module streams for php:remi-8.3

The Remi release package is missing, or DNF has stale module metadata. Reinstall the EL8 repository package, reset the PHP module, and enable the stream again.

sudo dnf clean all
sudo dnf install -y https://rpms.remirepo.net/enterprise/remi-release-8.rpm
sudo dnf module reset php -y
sudo dnf module enable php:remi-8.3 -y
sudo dnf install -y php php-fpm

Nginx returns 502 Bad Gateway

The PHP-FPM service may be stopped, or Nginx may be pointing at a socket that does not exist. Confirm the socket and restart the pool before checking the Nginx error log.

sudo systemctl status php-fpm --no-pager
sudo ls -l /run/php-fpm/www.sock
sudo systemctl restart php-fpm nginx
sudo tail -n 30 /var/log/nginx/error.log

The installer says Permission denied

Chamilo needs write access to its configuration and upload locations, while SELinux must also label those paths as writable. Reapply the ownership and context commands from Step 3, then restart PHP-FPM.

sudo chown -R nginx:nginx /var/www/chamilo/app /var/www/chamilo/web /var/www/chamilo/main/default_course_document/images /var/www/chamilo/main/lang
sudo restorecon -Rv /var/www/chamilo
sudo systemctl restart php-fpm

Courses or document downloads return 404 Not Found

This usually means the request is reaching a different Nginx server block or the Chamilo route block was edited incorrectly. Check the active configuration for the domain and reload only after the syntax test passes.

sudo nginx -T | grep -A3 -B2 'server_name YOUR_DOMAIN'
sudo nginx -t
sudo systemctl reload nginx

Hardening and maintenance

  • Back up the chamilo database and /var/www/chamilo before updating the application; course uploads are part of the site data.
  • Keep SELinux enforcing and leave /var/www/chamilo/app/config owned by root:nginx after the installer has finished.
  • Test certificate renewal with sudo certbot renew --dry-run and keep the server packages updated with sudo dnf update.
  • Restrict SSH to key-based administrator access where possible, and do not open MariaDB’s port to the public internet.

FAQ

Why use PHP 8.3 on Rocky Linux 8?

Rocky Linux 8’s base PHP stream can be older than the supported target for the 1.11 branch. The Remi 8.3 stream provides the PHP runtime and matching extensions without mixing individual packages from unrelated repositories.

Can I use an IP address instead of a domain?

You can use an IP for a short local test, but the HTTPS command in this guide expects a DNS name that Certbot can validate. Use a domain for a public learning portal and keep the certificate renewal process working.

Does MariaDB need port 3306 open?

No. The Chamilo database account is restricted to localhost, and PHP-FPM connects locally. Leaving port 3306 closed prevents outside clients from reaching MariaDB.

What should I back up for a Chamilo migration?

Back up the chamilo database and the application directory, especially uploaded course content and /var/www/chamilo/app/config. Restore the database and files together so course records do not point to missing uploads.

Samrat Ghosh
Written by

Samrat Ghosh

Founder & Infrastructure Engineer

Samrat Ghosh is the founder of VPSLake, where he builds and runs the remote desktop and VPS hosting infrastructure the company is built on. He writes hands-on guides about RDP, Windows Server, VPS management and secure remote access - the practical documentation he wishes had…

Previous Article

5 Best XenaxCloud Alternatives in 2026

Next Article

How Much Does It Cost to Run a Minecraft Server Per Month?

Write a Comment

Leave a Comment

Your email address will not be published. Required fields are marked *

Subscribe to our Newsletter

Subscribe to our email newsletter to get the latest posts delivered right to your email.
Pure inspiration, zero spam ✨