Skip to content

Installing HiLMS without Docker

For a machine that already runs PHP-FPM and a web server. Commands are for Debian 13 and Ubuntu 24.04; read requirements.md first, storage.md if media will live in a bucket, languages.md if this installation will speak more than one language, mcp.md if AI agents will author courses here, and theming.md if this installation runs a theme of its own.

Debian and Ubuntu do not ship 8.5 yet, so take it from the sury repository:

sudo apt-get update
sudo apt-get install -y ca-certificates apt-transport-https lsb-release curl gnupg
curl -fsSL https://packages.sury.org/php/apt.gpg | sudo gpg --dearmor -o /usr/share/keyrings/sury-php.gpg
echo "deb [signed-by=/usr/share/keyrings/sury-php.gpg] https://packages.sury.org/php/ $(lsb_release -sc) main" \
| sudo tee /etc/apt/sources.list.d/sury-php.list
sudo apt-get update
sudo apt-get install -y \
php8.5-fpm php8.5-cli php8.5-common php8.5-opcache \
php8.5-bcmath php8.5-curl php8.5-gd php8.5-intl php8.5-mbstring \
php8.5-mysql php8.5-redis php8.5-xml php8.5-zip

ctype, dom, exif, fileinfo, filter, iconv, json, libxml, openssl, pcntl, pcre, pdo, posix, session, simplexml, sodium, tokenizer, xmlreader, xmlwriter and zlib come with those packages. Confirm with php -m.

Then set the ini values from requirements.md in /etc/php/8.5/fpm/conf.d/99-hilms.ini and /etc/php/8.5/cli/conf.d/99-hilms.ini:

memory_limit = 256M
max_execution_time = 60
upload_max_filesize = 100M
post_max_size = 100M
date.timezone = UTC
opcache.enable = 1
opcache.validate_timestamps = 0

Either MySQL 8.4 from the MySQL APT repository, or MariaDB 11 from the distribution:

sudo apt-get install -y mariadb-server mariadb-client
sudo mariadb -e "CREATE DATABASE hilms CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
sudo mariadb -e "CREATE USER 'hilms'@'localhost' IDENTIFIED BY 'a-long-password';"
sudo mariadb -e "GRANT ALL ON hilms.* TO 'hilms'@'localhost';"

Set DB_CONNECTION=mariadb for MariaDB, mysql for MySQL. The dump client must match: mariadb-dump or mysqldump.

sudo apt-get install -y redis-server
sudo sed -i 's/^# *maxmemory-policy .*/maxmemory-policy noeviction/' /etc/redis/redis.conf
sudo sed -i 's/^# *maxmemory .*/maxmemory 256mb/' /etc/redis/redis.conf
sudo systemctl restart redis-server

noeviction matters: the queue lives in Redis, and an evicted job is a lost job. The limit matters too: a full Redis refuses writes, which HiLMS rides out on the database, while an unbounded one grows until the kernel kills the largest process on the machine — often the database. 256 MB is plenty; the Redis memory health check warns at 80 % of it.

sudo apt-get install -y nginx composer git unzip
sudo apt-get install -y jpegoptim optipng pngquant webp # optional, smaller images

Node 24 is only needed where the assets are built. Build them on a workstation and copy public/build across if you would rather not install Node on the server.

sudo mkdir -p /var/www/hilms && sudo chown "$USER" /var/www/hilms
git clone [email protected]:pipejesus/hilms.git /var/www/hilms
cd /var/www/hilms
git checkout v0.6.0
composer install --no-dev --optimize-autoloader
npm ci && npm run build # or copy public/build from elsewhere
cp .env.production.example .env # then edit it
php artisan hilms:install

hilms:install checks the machine, generates the key, migrates, seeds the roles and permissions, asks which time zone the panel is read in (or takes it from --timezone), asks for the first administrator (or takes it from HILMS_ADMIN_*), links public storage, generates the API keys, warms the caches and runs the health checks.

Ownership: the web user must own storage and bootstrap/cache.

sudo chown -R www-data:www-data /var/www/hilms/storage /var/www/hilms/bootstrap/cache
server {
listen 443 ssl http2;
server_name lms.example.com;
root /var/www/hilms/public;
index index.php;
ssl_certificate /etc/letsencrypt/live/lms.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/lms.example.com/privkey.pem;
client_max_body_size 100m;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.5-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* { deny all; }
error_page 404 /index.php;
}

The Caddy equivalent:

lms.example.com {
root * /var/www/hilms/public
encode gzip
php_fastcgi unix//run/php/php8.5-fpm.sock
file_server
request_body {
max_size 64MB
}
}

Course material — the media library’s files that only a lesson may show — lives outside the document root and is streamed by PHP by default. On a large installation, let nginx serve the bytes instead: set MEDIA_SENDFILE=nginx and add an internal location to the server block. Nothing reaches it from the outside; the application names the file after it has checked the student’s access. Only the local lessons disk is handed over this way; course material kept on any other disk is streamed by PHP, or sent to its bucket with a five-minute address (storage.md).

location /_lessons/ {
internal;
alias /var/www/hilms/storage/app/private/lessons/;
}

Caddy has no X-Accel-Redirect; leave MEDIA_SENDFILE=php there. The Docker image ships the nginx snippet already (/etc/nginx/server-opts.d/lessons.conf), so setting MEDIA_SENDFILE=nginx is all a Compose stack needs.

In /etc/php/8.5/fpm/pool.d/www.conf, size the pool to the memory you have: each child peaks near memory_limit. On a 4 GB machine, pm = dynamic, pm.max_children = 10, pm.start_servers = 2, pm.min_spare_servers = 1, pm.max_spare_servers = 3.

Horizon must run as one supervised process. With supervisor:

; /etc/supervisor/conf.d/hilms-horizon.conf
[program:hilms-horizon]
process_name=%(program_name)s
command=php /var/www/hilms/artisan horizon
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/hilms-horizon.log
stopwaitsecs=3600
sudo supervisorctl reread && sudo supervisorctl update

The systemd equivalent:

; /etc/systemd/system/hilms-horizon.service
[Unit]
Description=HiLMS queue workers
After=network.target redis-server.service
[Service]
User=www-data
Restart=always
ExecStart=/usr/bin/php /var/www/hilms/artisan horizon
ExecStop=/usr/bin/php /var/www/hilms/artisan horizon:terminate
TimeoutStopSec=3600
[Install]
WantedBy=multi-user.target
sudo crontab -u www-data -e
* * * * * cd /var/www/hilms && php artisan schedule:run >> /dev/null 2>&1

Everything operational hangs off that minute: the health heartbeats, the nightly backup at 01:30, its cleanup and monitor, the audit-log and failed-job pruning. Without it the health page turns red within minutes, which is the point.

See upgrade.md.

Untested, but the shape is known. Install lsphp85 and the same extension packages (lsphp85-common, lsphp85-mysql, lsphp85-intl, lsphp85-redis, …), point the virtual host document root at /var/www/hilms/public, let it read the shipped public/.htaccess for the rewrite, raise Max Request Body Size to 64M, and make sure the front proxy sends X-Forwarded-Proto. Report anything that differs so this section can lose its warning.

HiLMS is MIT-licensed. No replicants were harmed in the writing of these books.