Requirements
What a machine must offer before HiLMS runs on it. Hilms\Ops\Support\Requirements is the
same list in code: the health checks and hilms:install read it, and
app-modules/ops/tests/Feature/RequirementsTest.php fails when this page and the code drift
apart.
The quickest way to check a machine is to install and let it tell you:
php artisan hilms:installphp artisan health:checkPHP 8.5, newest patch release. Every extension below is a require entry in composer.json,
so composer install refuses to run on a host that misses one.
| Extension | Extension | Extension | Extension |
|---|---|---|---|
| ctype | curl | dom | exif |
| fileinfo | filter | gd | iconv |
| intl | json | libxml | mbstring |
| openssl | pcntl | pcre | pdo |
| pdo_mysql | posix | redis | session |
| simplexml | sodium | tokenizer | xml |
| xmlreader | xmlwriter | zip | zlib |
Recommended but not required: opcache (much faster) and bcmath. imagick and ffmpeg
are not used.
php.ini
Section titled “php.ini”| Setting | Value | Why |
|---|---|---|
upload_max_filesize |
at least UPLOAD_MAX_KILOBYTES, 100M recommended |
The panel caps uploads at the smallest of this, post_max_size, Livewire’s rule and the media library setting |
post_max_size |
the same | See above |
memory_limit |
256M for the web, 512M for the workers | Image conversions |
max_execution_time |
60 | Uploads and conversions run on the queue |
date.timezone |
UTC |
HiLMS stores everything in UTC |
opcache.enable |
1 | |
opcache.validate_timestamps |
0 in production | Restart PHP-FPM after a deployment |
Binaries
Section titled “Binaries”| Binary | Needed for | Notes |
|---|---|---|
mysqldump or mariadb-dump |
Nightly backups | The one that matches DB_CONNECTION; set DB_DUMP_BINARY_PATH when it is not on the PATH |
jpegoptim, optipng, pngquant, cwebp |
Smaller images | Optional; the media library skips the ones it cannot find |
The MariaDB client provides both names and dumps MySQL 8.4 as well, which is what the Docker image ships.
Services
Section titled “Services”| Service | Version | Notes |
|---|---|---|
| MySQL | 8.0 or newer, 8.4 tested | DB_CONNECTION=mysql |
| MariaDB | 10.11 or newer, 11 tested | DB_CONNECTION=mariadb |
| Redis | 6.2 or newer, 7 tested | Cache, queue and Horizon; maxmemory set (256 MB is plenty) with maxmemory-policy noeviction, no cluster. Sessions live in the database, and the site keeps serving while Redis is away |
| Web server | nginx preferred | Document root public/; Caddy, Apache and OpenLiteSpeed work too |
| Queue worker | one php artisan horizon process under a supervisor |
|
| Scheduler | php artisan schedule:run every minute, or one schedule:work process |
|
| SMTP | outbound | Account setup links, enrolment mail and operational alerts |
| Node and npm | 24 | Build time only; a release ships public/build |
| Composer | 2 |
HTTPS is terminated by the host’s reverse proxy. Set APP_URL to the https address,
TRUSTED_PROXIES to the proxy, and SESSION_SECURE_COOKIE=true.
The file system
Section titled “The file system”| Path | Needs |
|---|---|
storage/ |
Writable by the web user, and by the user the workers run as |
bootstrap/cache/ |
Writable |
public/storage |
A relative symlink to storage/app/public, made by php artisan storage:link --relative (hilms:install writes it). An absolute one names the path the command ran at, which a container serving the application from somewhere else cannot follow |
storage/app/keys |
The Passport key pair; included in every backup |
storage/app/backups |
Where the nightly archives land |
storage/app/private/lessons |
Course material — the media library’s files only a lesson may show — on the local disk; never under public/, served only through signed links; included in every backup |
Disk: about 0.4 GB for the code and its dependencies, plus the uploaded media, plus roughly twice the size of one database dump and the media for every retained backup. The default retention keeps everything for a week, then daily copies for sixteen days, weekly for eight weeks and monthly for four months.
The clock runs in UTC. APP_KEY must be set once and never change: it decrypts the stored
OAuth tokens and the secrets kept in the panel’s Settings.
Media storage
Section titled “Media storage”Where uploads are kept is chosen in the panel, under Settings → Media and storage (administrators only):
- This server keeps course material in
storage/app/private/lessonsand everything else instorage/app/public. Nothing else to configure; both directories are in every backup. - An S3-compatible bucket (Cloudflare R2, Backblaze B2, Wasabi, Amazon S3) takes the
course material, under
lessons/in the lesson bucket. The bucket stays private: a student’s link still goes through HiLMS, which checks lesson access and only then redirects to a presigned address that lives five minutes. Public media — course covers, avatars, the logo, page images — follows only when the page also names a public bucket of its own and its public address (an R2 custom domain orr2.devaddress, a CDN in front of it); otherwise it stays on this server. The public bucket may never be the lesson bucket: R2 and B2 open a whole bucket to anyone once it has a public address, lesson files included.
The page’s guide names the provider (Cloudflare R2, Amazon S3, Backblaze B2, Hetzner Object
Storage, DigitalOcean Spaces, or any other S3-compatible service) and checks the bucket before it
saves. For Other S3-compatible, a field left blank falls back to MEDIA_S3_* in .env, and
the page is seeded from them (and from MEDIA_LESSONS_DISK) when hilms:install or
hilms:upgrade first migrates, so an installation configured in .env keeps working. A change
applies to new uploads at once; files already uploaded keep working where they are and move only
when the page’s move button is pressed (storage.md). The health check
MediaStorage asks the bucket about one file every run.
Livewire’s temporary uploads stay on the local disk (FILESYSTEM_DISK=local) whatever is
chosen: every upload rule — the content sniffing, the video and audio checks, the caption
parser, the SVG sanitiser — reads a local file, and the file goes to the bucket when the form
is saved. The server therefore still needs room for the largest upload in flight.
What the buckets need — storage.md walks through creating them with Cloudflare R2 and Backblaze B2, fills in the page and checks the result:
-
An access key limited to the two buckets, allowed to list, read, write and delete objects. Every named provider needs the key and the secret; only Other S3-compatible may leave them to the environment, or to a server whose role already grants access (an EC2 instance profile).
-
A CORS rule allowing
GETandHEADfromAPP_URL, with theRangeheader, on the lesson bucket and on the public bucket when there is one. Once a file lives in a bucket, the panel’s upload field fetches it across origins to preview it, and the video player asks for the video and its caption tracks in CORS mode (crossorigin="anonymous", or the tracks would be thrown away); without the rule a video from the bucket does not play at all. The guide hands out the rule with the site’s address filled in, in the shape the provider takes; for R2 and S3:[{"AllowedOrigins": ["https://lms.example.com"],"AllowedMethods": ["GET", "HEAD"],"AllowedHeaders": ["Range"],"MaxAgeSeconds": 3600}] -
Nothing public on the lesson bucket: no public address, no public-read policy.
-
Both buckets under one account and one endpoint, since the page has one of each.
The content security policy lets the browser follow to the configured buckets’ addresses on its own; nothing needs adding to it.
Environment reference
Section titled “Environment reference”Every key of .env.production.example, which is the file to copy when installing.
| Key | Default | What it does |
|---|---|---|
APP_NAME |
HiLMS | Shown in the panel, the mail and the backup directory name |
APP_ENV |
production | production turns on the production-only health checks and turns off strict Eloquent |
APP_KEY |
— | Generated once by hilms:install; changing it makes stored OAuth tokens unreadable |
APP_DEBUG |
false | Never true in production |
APP_URL |
— | The public https address; every generated link uses it |
APP_LOCALE |
pl | The language hilms:install seeds as the installation’s first and default one; Settings → Languages is the list from then on (languages.md) |
APP_FALLBACK_LOCALE |
en | Used for missing translations; stays en, because the code’s own language is English |
APP_FAKER_LOCALE |
pl_PL | Demo data only |
APP_MAINTENANCE_DRIVER |
file | How artisan down remembers it is down |
LOG_CHANNEL |
stack | |
LOG_STACK |
daily | One file per day under storage/logs |
LOG_DAILY_DAYS |
14 | How many days of logs to keep |
LOG_DEPRECATIONS_CHANNEL |
null | |
LOG_LEVEL |
info | |
DB_CONNECTION |
mysql | mysql or mariadb |
DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD |
— | The database |
DB_ROOT_PASSWORD |
— | The MySQL root password the Compose stack starts the database with; read only by compose.yaml, never by the application |
DB_DUMP_BINARY_PATH |
empty | Directory of the dump binary, when it is not on the PATH |
DB_DUMP_SKIP_SSL |
true in the Docker stack | Skips TLS for the backup dump; only right when the database is on a private network |
DB_DUMP_SSL_FLAG |
skip-ssl | skip-ssl, ssl-mode=DISABLED or ssl-mode=PREFERRED, whichever the dump client understands |
SESSION_DRIVER |
database | Sessions outlive a Redis outage |
SESSION_LIFETIME |
120 | Minutes |
SESSION_ENCRYPT |
false | |
SESSION_PATH, SESSION_DOMAIN |
/ , null | |
SESSION_SECURE_COOKIE |
true | Requires https |
FILESYSTEM_DISK |
local | The framework’s default disk, and where Livewire keeps an upload until the form is saved. It stays local: every upload rule reads the file from the local disk. Media never goes there (see MEDIA_DISK and MEDIA_LESSONS_DISK) |
UPLOAD_MAX_KILOBYTES |
102400 | The upload ceiling the panel enforces and shows; every upload field is capped to it |
PHP_UPLOAD_MAX_FILE_SIZE, PHP_POST_MAX_SIZE, NGINX_CLIENT_MAX_BODY_SIZE |
100M | Read by the Docker image’s PHP and nginx, not by the application; must allow at least UPLOAD_MAX_KILOBYTES. A manual installation sets the same values in php.ini and the web server |
QUEUE_CONNECTION |
failover | Redis for Horizon; while Redis cannot answer, a job runs right after the response. An assistant request and a file move wait for a worker or are refused |
REDIS_QUEUE_RETRY_AFTER |
330 | Must stay above the longest timeout of the supervisors on the redis connection (300); media moves have a connection of their own, media-moves, with an hour per file |
MEDIA_QUEUE |
media | The queue image conversions run on |
QUEUE_CONVERSIONS_BY_DEFAULT |
true | Conversions are queued, not made during the request |
CACHE_STORE |
failover | Redis, and the database while Redis cannot answer |
SETTINGS_CACHE_ENABLED |
true | Caches the settings table |
SETTINGS_CACHE_MEMO |
true | Keeps resolved settings in memory for one request |
REDIS_CLIENT |
phpredis | |
REDIS_HOST, REDIS_PORT, REDIS_PASSWORD |
— | Redis |
REDIS_TIMEOUT |
2 | Seconds to wait for a connection to Redis; without it an unreachable host holds every call for a minute |
MAIL_MAILER |
smtp | |
MAIL_SCHEME |
null | smtp or smtps |
MAIL_HOST, MAIL_PORT, MAIL_USERNAME, MAIL_PASSWORD |
— | The SMTP server |
MAIL_FROM_ADDRESS, MAIL_FROM_NAME |
— | The sender; Settings → Mail in the panel overrides both |
BREVO_API_KEY |
empty | The API key of a Brevo account, for the brevo transport. Resend, Postmark, Mailgun and SES keep Laravel’s own names in config/services.php (RESEND_API_KEY, POSTMARK_API_KEY, MAILGUN_DOMAIN with MAILGUN_SECRET and MAILGUN_ENDPOINT, AWS_ACCESS_KEY_ID with AWS_SECRET_ACCESS_KEY and AWS_DEFAULT_REGION), and a key stored on the mail settings page takes precedence over all of them |
MAIL_ALWAYS_TO |
empty | While it is set, every message goes to this address instead of the person it names — a pin for a staging installation. Settings → Mail shows it and overrides it |
TRUSTED_PROXIES |
* behind a loopback-only proxy |
Comma separated addresses, or *; empty ignores forwarded headers |
CORS_ALLOWED_ORIGINS |
empty | Browser origins allowed to call /api/v1 and /oauth/token; empty allows none |
OPS_NOTIFICATION_EMAIL |
empty | Where Horizon, the health checks and the backups report; empty sends nothing |
HEALTH_SECRET_TOKEN |
empty | Required in the X-Secret-Token header of GET /health; while it is empty the endpoint answers 403 to everybody, and in production the configuration health check fails. The endpoint answers 503 only when a check failed, 200 for a warning |
PULSE_ENABLED |
true | Performance recording |
TELESCOPE_ENABLED |
false | Telescope is a development dependency and never registers in production |
ACTIVITYLOG_ENABLED |
true | The audit trail |
ACTIVITYLOG_CLEAN_AFTER_DAYS |
365 | How long audit entries are kept |
BACKUP_KEEP_ALL_DAYS |
7 | Every archive is kept this long |
BACKUP_KEEP_DAILY_DAYS |
16 | Then one a day |
BACKUP_KEEP_WEEKLY_WEEKS |
8 | Then one a week |
BACKUP_KEEP_MONTHLY_MONTHS |
4 | Then one a month |
BACKUP_KEEP_YEARLY_YEARS |
0 | No yearly copies: a deleted account must not live on in a dump |
BACKUP_ARCHIVE_PASSWORD |
empty | Encrypts every backup archive when set; empty means unencrypted archives |
BACKUP_S3_ENABLED |
false | Copies each backup to S3 as well |
BACKUP_S3_KEY, BACKUP_S3_SECRET, BACKUP_S3_BUCKET, BACKUP_S3_ENDPOINT |
— | The S3-compatible target |
BACKUP_S3_REGION |
us-east-1 | The bucket’s region; R2 reads us-east-1 as auto |
BACKUP_S3_PATH_STYLE |
false | true for a provider that wants the bucket in the path rather than the host name (MinIO, some self-hosted stores) |
HILMS_THEME |
hilms | The theme every student page is rendered from, a directory under themes/. A mounted client theme is bound into the container; see theming.md |
HILMS_STYLES_ENFORCE_ACCESSIBILITY |
false | Contrast and the accessibility floors are advice in the Styles editor; true makes a style that falls short impossible to activate, and the active style impossible to save into falling short. An operator’s promise, so it is not a panel setting; see theming.md |
HILMS_ADMIN_NAME, HILMS_ADMIN_EMAIL, HILMS_ADMIN_PASSWORD |
— | Used once, by the first hilms:install, to create the administrator |
HILMS_API_DEMO_SECRET |
empty | Seeds a demo API client locally; leave empty in production |
HILMS_PORT |
8080 | The loopback port the Compose stack publishes and the host’s reverse proxy talks to; read only by compose.yaml |
HILMS_VERSION |
commented out | The image tag the Compose stack runs, and — Compose passes .env into the containers — the version artisan about reports. Uncomment it to pin the tag being installed; left commented, the stack runs latest and every container reports the version its own image was built with |
HILMS_AUTO_INSTALL |
set per service | Whether the container runs hilms:deploy as it starts: hilms:install on a database that holds no installation, hilms:upgrade on one that does. compose.yaml sets it on each service rather than in .env, which reaches them all: true on app, false on horizon and scheduler. Outside Compose, set it on the one container that deploys |
BUNNY_STREAM_TOKEN_KEY |
empty | The Token Authentication key of a Bunny Stream library; with it every video embed carries a token that expires within the hour. The key stored under Settings → Media and storage takes precedence |
SCOUT_DRIVER |
database | The catalogue search engine; database searches the courses table itself and needs nothing else |
MEDIA_DISK |
public | The disk holding public media (the media library’s public files — course covers and page pictures — avatars and the branding) until Settings → Media and storage is saved; the page decides from then on (see Media storage) |
MEDIA_LESSONS_DISK |
lessons | The disk holding course material until the page is saved; private, served only through signed links. remote-lessons seeds the page with a bucket; any other disk seeds it with this server, and a local disk of your own is kept while that is chosen |
MEDIA_SENDFILE |
php | php streams a private file through the application, nginx hands a file on the lessons disk to nginx with an internal X-Accel-Redirect (the image ships the snippet). A file in a bucket is always answered with a five-minute presigned address once the access check passes |
MEDIA_S3_KEY, MEDIA_S3_SECRET, MEDIA_S3_BUCKET, MEDIA_S3_ENDPOINT |
— | The S3-compatible storage behind remote-lessons (lesson files under lessons/): an access key, its secret, the lesson bucket and the provider’s endpoint (https://<account>.r2.cloudflarestorage.com for R2). Fallbacks for fields left blank on the Media and storage page |
MEDIA_S3_PUBLIC_BUCKET |
empty | The bucket of its own that remote-public keeps public media in, under public/; never the lesson bucket |
MEDIA_S3_REGION |
us-east-1 | The bucket’s region; R2 reads us-east-1 as auto |
MEDIA_S3_PATH_STYLE |
false | true for a provider that wants the bucket in the path rather than the host name |
MEDIA_S3_PUBLIC_URL |
empty | The public bucket’s address (an R2 custom domain or r2.dev address, a CDN), which public media is read from |
AI_PROVIDER |
deepseek | The AI provider the editor assistants prompt, a key of ai.providers in config/ai.php; the panel’s Settings → AI assistants overrides it |
AI_MODEL |
deepseek-flash | The text model the assistants prompt, whichever provider is chosen; Settings → AI assistants overrides it. The SDK’s own per-provider defaults are never used, because they can name a retired model |
DEEPSEEK_API_KEY |
empty | The key of the default provider. Every other provider keeps the name config/ai.php reads it under (OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY, MISTRAL_API_KEY, GROQ_API_KEY, XAI_API_KEY, OPENROUTER_API_KEY, OLLAMA_API_KEY, OPENAI_COMPATIBLE_API_KEY, and the Azure and Bedrock sets), and each may take a *_URL for a proxy. A key stored under Settings → AI assistants takes precedence over all of them |
CSP_ENABLED |
true | The content security policy; turn it off only to prove it is the cause of a problem |
CSP_REPORT_ONLY |
false | Sends the front-end policy as report-only, so violations appear in the console without being blocked |
FILAMENT_IMPERSONATE_REDIRECT |
empty | Where an administrator lands once they are seeing the site as somebody else; empty means the page the installation chose for its students |
PASSKEYS_USER_HANDLE_SECRET |
empty | Derives the WebAuthn user handle of every passkey; empty means APP_KEY, so rotating APP_KEY invalidates every registered passkey |
PERSONAL_DATA_EXPORT_DELETE_AFTER_DAYS |
5 | How long a personal data archive stays downloadable; personal-data-export:clean removes the older ones nightly |
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET |
empty | Google sign-in; the button appears only when the id is set |
FACEBOOK_CLIENT_ID, FACEBOOK_CLIENT_SECRET |
empty | Facebook sign-in |
VITE_APP_NAME |
${APP_NAME} |
Build time only |
HiLMS is MIT-licensed. No replicants were harmed in the writing of these books.