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
firewalldpolicy. - 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
chamilodatabase and/var/www/chamilobefore updating the application; course uploads are part of the site data. - Keep SELinux enforcing and leave
/var/www/chamilo/app/configowned byroot:nginxafter the installer has finished. - Test certificate renewal with
sudo certbot renew --dry-runand keep the server packages updated withsudo 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.