Skip to content

Environment

.lando.yml defines the app hilms on the Laravel recipe, and everything it runs is a server:

Service Details Answers on
appserver PHP 8.5 behind nginx, serving public/ https://hilms.lndo.site
database MySQL 8.4; laravel for the site, hilms_testing* for the suites 127.0.0.1:33061
mariadb MariaDB 11.8, the same credentials, for bin/pest-mariadb 127.0.0.1:33062
cache Redis 7 for the cache and the queue (sessions are in the database) 127.0.0.1:63791
node Node 24, publishing the Vite dev server http://localhost:5173
mailpit Catches all mail SMTP 127.0.0.1:10251, UI http://mail.hilms.lndo.site
horizon A cli service running artisan horizon the way production does
scheduler A cli service running artisan schedule:work

The two worker services drop to www-data before running, because Lando runs a service command as root and everything they write into storage/ — conversions, caches, logs, backup archives — would otherwise be root-owned and unwritable by the appserver. Ownership is not the problem it looks like: www-data in the appserver is uid 1000, the developer’s own uid, so both sides read and write the same files.

The ports are off the defaults on purpose, so no other Lando app and no local server can already hold them. On start, Lando creates the test databases, grants laravel every privilege on `hilms\_testing%` on both engines, and generates the Passport key pair if it is missing. The Laravel recipe’s default MySQL configuration contains a variable MySQL 8.4 removed; .lando/mysql.cnf is the corrected copy.

Lando’s own tooling is deliberately short: lando dev, lando npm, lando node, lando horizon, lando queue, lando mysql, lando mariadb, plus the recipe’s lando artisan, lando composer and lando ssh.

bin/host is the single door to the host tooling, and the one place that knows the ports. It:

  • resolves the checkout it lives in, so the same script serves the main tree and every sub-tree;
  • exports DB_HOST, DB_PORT (33061 for mysql, 33062 for mariadb, read from DB_CONNECTION or .env), REDIS_HOST, REDIS_PORT, MAIL_HOST and MAIL_PORT — each yielding to anything already exported, which is how bin/pest-mariadb reaches the other engine and how CI keeps its own addresses;
  • points the five caches Laravel and two packages read — APP_CONFIG_CACHE, APP_ROUTES_CACHE, APP_EVENTS_CACHE, APP_MODULES_CACHE and APP_ICONS_CACHE — at bootstrap/cache/host/, a directory nothing creates (below);
  • points PHP_INI_SCAN_DIR at .lando/php.ini with a leading colon, so PHP’s own scan directory is kept and the project’s settings are laid on top;
  • puts the tree’s vendor/bin on PATH, and execs.

An environment variable beats .env in Laravel, which is why .env keeps the hostnames the containers talk to each other by and needs no second copy.

Every named wrapper is one line over it — bin/pest, bin/pest-mariadb, bin/stan, bin/pint, bin/rector, bin/artisan, bin/composer, bin/test-databases, bin/tree, and bin/gates over the five checks — so a new one is trivial, and nothing else may hard-code a port. bin/outdated lists the advisories against what is installed and every direct package with a newer release (Quality has the rule that goes with it).

Two wrappers do more than that:

  • bin/pest names the tree’s own test database, sets HILMS_CACHES=testing so the suite reads caches of its own, and runs six processes by default (Quality).
  • bin/artisan refuses optimize and config:cache, and adds --no-optimize to hilms:install, hilms:upgrade and hilms:deploy, all three of which end by warming the caches. The host reads no cache, so a cache written there would serve nobody — and one written with the host’s addresses would hold 127.0.0.1:33061, which inside the container is the container.

One more command belongs to lando artisan on an installation whose panel names its mail server: hilms:mail:test. The stored mail settings win over the environment by design, and here they say mailpit:1025, a name only the containers resolve.

.lando/php.ini is the single source of the PHP settings the project depends on (upload_max_filesize, post_max_size, opcache.enable_cli): .lando.yml hands it to the container, bin/host to the host, and ci.yml to the runner, all through the same variable. App\Support\UploadLimits reads them, so they must agree or the upload tests disagree about what the system accepts.

The host and the container never share a cache

Section titled “The host and the container never share a cache”

optimize bakes the paths and the addresses of the machine that ran it into bootstrap/cache/: the configuration, the routes, the events, and two package caches — internachi/modular’s list of modules and blade-icons’ manifest. The container reads this checkout at /app and the host at its own path, so a cache one side writes names files the other cannot open. So the two sides never read the same cache files:

  • The container reads the default ones in bootstrap/cache/. lando artisan optimize warms them, and it is always safe to run: nothing on the host reads what it writes.
  • The host reads the files bin/host names under bootstrap/cache/host/, a directory nothing creates — which is to say it reads no cache and loads everything fresh. bin/artisan refuses to write one.
  • The suite reads bootstrap/cache/testing/, which does not exist either (Quality).

Laravel reads the first three variables itself. The other two are ours: modular and blade-icons hard-code their cache files, so AppServiceProvider::relocatePackageCaches() rebinds InterNACHI\Modular\Support\Cache and BladeUI\Icons\IconsManifest to the path in APP_MODULES_CACHE and APP_ICONS_CACHE when the variable is set. The container never sets it and keeps the packages’ defaults. services.php and packages.php stay shared: they are Composer’s artifacts, not the environment’s.

Leave the site’s caches cold locally, where it serves perfectly well without them, and warm them with lando artisan optimize when you want to measure something that needs them. After changing configuration, routes or a module’s provider on a warmed site, run lando artisan optimize again, or lando artisan optimize:clear: the host’s commands see the change straight away, the site sees what the container cached.

A batch or a fix lives in its own git worktree under ../hi-lms-trees, created by bin/tree new <branch>. Each tree has its own vendor/, node_modules/, public/build, CodeGraph index, test database (hilms_testing_<tree>) and bin/ — which is why the wrappers need no tree argument. Nothing is mounted into a container; nginx and the browser serve the main tree only. Quality covers the commands.

npm run build runs on the host and compiles into public/build: the core entries (resources/js/app.js, the design catalogue’s resources/css/design.css) plus, for every themes/*/theme.json marked "build": "bundled", that theme’s entries, its preview stylesheet and its declared fonts. Run it after changing Blade or CSS.

lando dev runs Vite with hot module replacement on http://localhost:5173, published by the node service. It is deliberately not proxied through a .lndo.site hostname: such a proxy only speaks plain http, and an https page may load a script from no http origin but localhost. While it runs, the content security policy adds the origin in public/hot and in every themes/*/public/hot (see Security and privacy), so the policy stays on in development.

.env.example is complete for a local checkout. Beyond the framework’s own keys:

Key Means
APP_LOCALE=pl, APP_FALLBACK_LOCALE=en The language hilms:install seeds as the first and default one, and what a missing translation falls back to. Nothing reads APP_LOCALE once the Languages page holds a row (Languages)
HILMS_THEME The active theme (default hilms)
HILMS_DESIGN_CATALOGUE Opens /design outside production
HILMS_STYLES_ENFORCE_ACCESSIBILITY Refuses to activate a style that falls short of the contrast pairs or the floors, and to save the active one into falling short; off, the Styles editor only advises (Styles)
HILMS_ADMIN_NAME, HILMS_ADMIN_EMAIL, HILMS_ADMIN_PASSWORD The seeded administrator; an empty password skips it
HILMS_API_DEMO_SECRET Seeds the demo API client for the SDK and the plugin
HILMS_TEST_PROCESSES How many workers bin/pest runs
UPLOAD_MAX_KILOBYTES The upload ceiling Livewire and the media library share
MEDIA_LESSONS_DISK, MEDIA_SENDFILE, MEDIA_QUEUE Where lesson files live, who serves them, which queue converts images
BUNNY_STREAM_TOKEN_KEY Token authentication for the Video block’s Bunny Stream source
AI_PROVIDER, AI_MODEL, <PROVIDER>_API_KEY The fallback assistant configuration; the panel overrides all of it
HEALTH_SECRET_TOKEN Required in production; /health answers 403 while it is empty
CSP_ENABLED, CSP_REPORT_ONLY Turn the policy off, or move the front-end one into report-only
GOOGLE_*, FACEBOOK_* OAuth credentials

docs/install/requirements.md is the full reference, and .env.production.example the production template.

bin/artisan storage:link --relative links public/storage to storage/app/public; hilms:install writes exactly that link. Relative and never absolute, because an absolute one names the path the command happened to run at, which is not where the server that follows it sees the application — written on the host it points into a home directory a container knows nothing about, it dangles, and every uploaded file answers 404.

Course material — a library file only a lesson may show — goes to the private lessons disk under storage/app/private/lessons instead, and is served only through the signed media.show route. Image conversions and file moves run on the media queue, which the horizon service works. A local installation keeps everything on this server; pointing it at a real bucket for a test is Settings → Media and storage and docs/install/storage.md, and bin/artisan hilms:media:relocate --dry-run shows what would move (Media).

Lando’s development CA is trusted in Chrome on the development machine, so https://hilms.lndo.site works. If a browser complains, import ~/.lando/certs/LandoCA.crt into its trust store, or use http://.

The production shape is documented for operators, not here: docs/install/docker.md for the image and the Compose stack, docs/install/manual.md for PHP-FPM, nginx, supervisor and cron by hand, docs/install/upgrade.md for upgrading and rolling back. Both installation routes end in php artisan hilms:install (see Operations).

Two local notes about running the stack on the same machine as Lando: the production compose.yaml names its project hilms, which is also Lando’s project name, so run it as docker compose -p hilms-compose-test ... or Compose will adopt Lando’s containers; and the Lando app keeps ports 80 and 443, so a local stack must bind another one (HILMS_PORT=18080).

Symptom Fix
Failed to open stream after moving classes bin/composer dump-autoload (the optimised classmap is stale)
A panel page says a policy or permission is missing after adding a resource bin/artisan shield:generate --all --panel=admin, then seed the module permissions
Styles missing on a new view npm run build; Tailwind scans the theme’s views, not the module’s
A block preview is unstyled in the panel The atom is not scanned by the theme’s preview.css
A panel stylesheet or script change does nothing bin/artisan filament:assets; the panel’s assets are published verbatim and versioned by what that command last wrote (The block editor)
A host command dies with There is no existing directory at "/app/storage/logs" Something exported one of the five cache variables pointing at the container’s files, or ran PHP on the host without bin/host. Run it through the wrapper (above)
An installer prompt fails with NonInteractiveValidationException Run it yourself in a terminal, or pass every --admin-* option
A page 404s that should be a page slug The slug collides with a one-segment route; NotReservedSlug lists what is taken
The site reads a stale configuration, route or module — a page a merge just added answers 404 The container’s caches are warm: lando artisan optimize again, or lando artisan optimize:clear (never bin/artisan optimize — it refuses)
Database service unhealthy after changing .lando.yml lando destroy -y && lando start, then migrate and seed again
A run of the suite hangs on RefreshDatabase A killed Pest process still holds a metadata lock; see Quality

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