How to Install Chamilo with LEMP on AlmaLinux 8

Install Chamilo with LEMP Stack on AlmaLinux 8 using Nginx, MariaDB, PHP 8.3, SELinux-safe permissions, HTTPS, and Chamilo 1.11.40 for a secure portal.
A glowing e-learning dashboard connects an AlmaLinux server rack and database cylinder in a blue data center.

Install Chamilo with LEMP Stack on AlmaLinux 8 by putting Chamilo LMS 1.11.40 behind Nginx, MariaDB, PHP-FPM, SELinux, and HTTPS. With DNS already pointed to the VPS, you can reach the portal and create its first administrator in about 15–20 minutes.

Prerequisites

  • AlmaLinux 8.10 or another current AlmaLinux 8 update, with 2 vCPUs, at least 2 GB RAM, and 10 GB of free disk space.
  • A root account or a sudo-enabled user connected over SSH. The commands below use sudo.
  • A domain such as YOUR_DOMAIN with an A record pointing to the server. Ports 80 and 443 must be reachable from the internet for Let’s Encrypt.
  • A unique database password, a separate Chamilo administrator password, and an email address for certificate notices.
  • A fresh VPSLake Linux VPS with a public IPv4 address is suitable for this installation.
  • Do not run Apache on ports 80 or 443; this guide installs Nginx as the web server.

Step 1: Install PHP, Nginx, MariaDB, and firewalld

AlmaLinux 8 uses DNF, while Chamilo 1.11.40’s official download is compatible with PHP 8.3, so select the Remi PHP 8.3 stream before installing the stack.

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-xml php-zip php-ldap php-soap
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

The output should show PHP 8.3.x, an Nginx version, and a MariaDB client version. The exact patch numbers will change as the repositories publish updates.

Step 2: Create a dedicated MariaDB database

Chamilo stores users, courses, and settings in MariaDB, so give this portal a local-only account with privileges limited to its own database.

sudo mysql_secure_installation

When the hardening wizard asks, remove anonymous users, disable remote root login, remove the test database, and reload the privilege tables. Then create the database and account; replace YOUR_DB_PASSWORD before running the block.

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 final command should print a row named chamilo. Keep the database name, username, and password for the browser installer; MariaDB does not need a public firewall rule because the application connects through localhost.

Step 3: Download Chamilo 1.11.40 and set safe permissions

Use the project’s numbered release archive instead of an old development snapshot, then make only Chamilo’s documented upload and configuration directories writable by Nginx.

The official Chamilo download page lists 1.11.40 for PHP 7.4 and 8.3. Download that release and copy its files into /var/www/chamilo:

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_ROOT="$(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_ROOT"
sudo cp -a "$CHAMILO_ROOT"/. /var/www/chamilo/

Keep the application readable by Nginx, but grant it write access only where the installer and course uploads require it. This avoids the world-writable permissions used by many older Chamilo tutorials.

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

The persistent SELinux contexts allow Nginx and PHP-FPM to read the code and write Chamilo data without disabling SELinux.

Step 4: Configure Nginx for Chamilo routes

Chamilo 1.11 uses special routes for course files, SCORM content, badges, and clean URLs; a generic PHP front-controller rule is not enough. Create the server block below and replace YOUR_DOMAIN before running it.

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 @rewrite {
        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 @rewrite;
    }

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

    location ~ \.php$ {
        try_files $uri @rewrite;
        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 @rewrite;
    }

    location ^~ /app/config/ {
        deny all;
    }

    location ^~ /vendor/ {
        deny all;
    }

    location ~ /\.(?!well-known) {
        deny all;
    }

    location ^~ /tests/ {
        deny all;
    }

    location ^~ /main/inc/lib/javascript/bigupload/files/ {
        deny all;
    }

    location ~* /web\.config$ {
        deny all;
        return 403;
    }
}
NGINX
sudo nginx -t
sudo systemctl reload nginx

The syntax check should return syntax is ok and test is successful. The root must remain /var/www/chamilo; changing it to a subdirectory breaks Chamilo’s course and document routes.

Step 5: Open the firewall and enable HTTPS

Allow SSH for administration, HTTP for the ACME challenge, and HTTPS for the finished portal before requesting the certificate.

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

The service list should include ssh, http, and https. Replace YOUR_EMAIL with a monitored address; Certbot must resolve YOUR_DOMAIN to this VPS before validation can succeed. Chamilo’s official 1.11 installation guide also documents the PHP requirements, writable directories, and Nginx rewrite behavior used here.

Step 6: Finish the browser installation

The web wizard creates Chamilo’s configuration and administrator account after the server-side dependencies are ready.

Open https://YOUR_DOMAIN/main/install/index.php in your browser. When the database page appears, enter:

  • 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

Continue through the wizard, choose the portal language, and create a long, unique administrator password. If the installer reports that a directory is not writable, stop and correct the ownership and SELinux labels from Step 3 rather than making the whole document root writable.

After the dashboard loads, protect the generated configuration directory and remove the temporary archive:

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

Run these checks from SSH, then sign in to the HTTPS dashboard and create a small test course. The command checks prove that the services, PHP modules, Nginx configuration, and public TLS endpoint are working together.

sudo systemctl is-active nginx php-fpm mariadb firewalld
sudo nginx -t
php -v | head -n 1
php -m | grep -E 'curl|fileinfo|gd|intl|mbstring|mysqli|PDO|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 list can appear in a different order, and the HTTP status may be a redirect after installation. A working login, course page, and file upload are the final proof that Chamilo’s Nginx rewrites and writable directories are correct.

Troubleshooting

502 Bad Gateway

Nginx cannot reach PHP-FPM, commonly because the service is stopped or the pool socket is owned by another account. Confirm that /run/php-fpm/www.sock exists, then restart the pool and Nginx.

sudo systemctl status php-fpm --no-pager
sudo ls -l /run/php-fpm/www.sock
sudo systemctl restart php-fpm nginx

The installer reports a missing PHP extension

The PHP stream may have been changed after the packages were installed, or PHP-FPM may still have old workers. Reinstall the modules and restart PHP-FPM before refreshing the wizard.

sudo dnf install -y php-curl php-gd php-intl php-mbstring php-mysqlnd php-xml php-zip php-ldap php-soap
sudo systemctl restart php-fpm
php -m | grep -E 'curl|gd|intl|mbstring|mysqli|PDO|xml|zip'

Course links or document downloads return 404

Chamilo’s clean URLs depend on the long Nginx rewrite block, not only on try_files. Check that the active server block has root /var/www/chamilo, that YOUR_DOMAIN matches the request hostname, and reload after correcting it.

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

The installer shows Permission denied or uploads fail

The PHP process needs write access to app, web, main/default_course_document/images, and main/lang, and SELinux needs the matching writable context. Reapply both the ownership and labels from Step 3, then retry the upload.

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

Certbot cannot validate YOUR_DOMAIN

The DNS record may still point to another address, or an upstream firewall may block port 80. Check the public DNS result, confirm that HTTP is allowed in firewalld and the VPS control panel, then run Certbot again.

dig +short YOUR_DOMAIN
sudo firewall-cmd --list-services
sudo certbot --nginx --redirect --agree-tos --no-eff-email -m YOUR_EMAIL -d YOUR_DOMAIN

Hardening and maintenance

  • Keep AlmaLinux, PHP, Nginx, MariaDB, and Chamilo updated; back up the chamilo database and /var/www/chamilo/app, /var/www/chamilo/web, and uploaded course data before upgrades.
  • Leave SELinux enforcing and keep /var/www/chamilo/app/config owned by root:nginx with no write permission for the web process after installation.
  • Test certificate renewal with sudo certbot renew --dry-run and keep SSH limited to key-based administrator access where possible.
  • Chamilo’s official guide recommends a scheduled request to https://YOUR_DOMAIN/main/cron/run.php for queued notifications; add it only after confirming the endpoint works for your installation.

FAQ

Does Chamilo 1.11.40 support PHP 8.3?

Yes. The official download page identifies Chamilo 1.11.40 as a release for PHP 7.4 and PHP 8.3. This guide selects PHP 8.3 from Remi so AlmaLinux 8 does not fall back to an older default stream.

Can I install Chamilo without a domain?

You can test the portal through an IP address, but a domain is the practical choice for a public LMS. Let’s Encrypt’s normal HTTP validation in this guide requires a DNS name that resolves to the VPS.

Why does Chamilo need several writable directories?

The installer writes configuration and Chamilo creates course uploads, generated assets, and language data in those locations. The rest of the application remains owned by root and readable by Nginx, which reduces the damage a compromised web process could cause.

Why is the Nginx configuration longer than a typical PHP site?

Chamilo 1.11 has dedicated URLs for courses, SCORM files, document downloads, badges, and legacy PHP endpoints. The rewrite rules translate those URLs to the correct scripts while keeping ordinary static files fast to serve.

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

Dedicated Server Price in India: 2026 Cost Guide

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 ✨