Deploying
This is the generic procedure. The record of the actual production server lives in the repo at
docs/production-runbook.md, and is deliberately not part of this site.
What a server runs
Section titled “What a server runs”| Process | Command | Notes |
|---|---|---|
| Web | PHP-FPM behind nginx | DDEV runs nginx, so use it here too. public/.htaccess (Apache only) redirects every trailing-slash URL, which development never does |
| Queues | php artisan horizon |
Under a process supervisor, which restarts it. Give it a stop timeout longer than the longest job (3600 s) |
| Assistant worker | php artisan queue:work assistant --tries=1 --timeout=300 --memory=512 --max-time=3600 |
Horizon doesn’t work this connection |
| Websockets | php artisan reverb:start |
Browsers reach it through the web server. If you proxy it at /app/, no page or category may use that as a slug |
| Scheduler | php artisan schedule:run, every minute |
From cron |
| Redis | Turn persistence on: otherwise a restart loses queued jobs. Never use allkeys-lru, which can evict them |
|
| Meilisearch | With a production master key. The app uses an admin API key, not the master key | |
| MariaDB, Chromium | Chromium renders the document PDFs |
Deploying a release
Section titled “Deploying a release”Envoy.blade.php runs these steps: envoy run deploy, from the project on the host, not inside DDEV. It also fast-forwards
production to master first and publishes this docs site last.
In order, from the application’s directory:
# 1. Pause the scheduler for the length of the deploy# (for example a marker file its cron entry checks for).
# 2. New code and its dependencies.git pull --ff-onlycomposer install --no-dev --optimize-autoloader --no-interaction
# 3. Assets: the admin, then one build per theme.npm cinpm run buildnpm run build:defaultnpm run build:gopromo
# 4. Schema and roles.php artisan migrate --forcephp artisan roles:sync
# 5. Caches.php artisan route:cachephp artisan view:cachephp artisan event:cachephp artisan filament:optimize
# 6. Load the new code everywhere.# Reload PHP-FPM: with opcache's validate_timestamps off, old code runs until you do.php artisan horizon:terminate # the supervisor restarts it on the new code, after running jobs finishphp artisan queue:restart # the assistant's worker# Restart Reverb through its process supervisor.
# 7. Resume the scheduler.Notes on these steps:
- Every theme needs its own build. A new theme in
themes/means a new build command here. roles:syncon every deploy. See Authorization. It fails if a role lists a capability that no module registers.- Don’t run
config:cache. A closure inconfig/filament-flexible-content-blocks.php(seo_image) can’t be serialized, so the command fails. --no-devmeans no Faker, so nothing a production seeder runs may use a factory.
Rebuilds a release may need
Section titled “Rebuilds a release may need”These aren’t part of every deploy. Run them when the release calls for it:
| Command | When |
|---|---|
php artisan cms:reindex |
After changes to how site_documents is built, or a bulk data change that skipped model events. A few minutes on the full catalog |
php artisan catalog:search-rebuild |
After a full reindex, an import, or a release that changes the search documents. It builds a fresh index and swaps it in, so search keeps working throughout. A queue worker must be running for later edits to reach search |
php artisan catalog:search-settings |
When only relevance settings changed |
php artisan catalog:regenerate-photos |
After a change to photo rules. Unchanged photos are skipped |
First install
Section titled “First install”After the steps above, on an empty database:
php artisan storage:link.- Set
INITIAL_ADMIN_EMAILandINITIAL_ADMIN_PASSWORD, and seed the first admin:php artisan db:seed --class=AdminUserSeeder, or runLegacySeederif you import. With no password set, it prints a random one. - For a first import of a feed:
catalog:add-supplier <source>queues its new products as drafts by the category mappings, andcatalog:publish-supplier <source>publishes them. Follow withcms:reindexandcatalog:search-rebuild, since publishing in bulk skips observers. - At cutover, move the number sequences above the legacy ones with
sales:sequence <type> --next=<number>.
Details are in Commands.
Environment variables
Section titled “Environment variables”Names only: values belong in the server’s .env, written from scratch rather than copied from
development. Modules’ config/ files show the rest.
| Purpose | Variables |
|---|---|
| Application | APP_ENV=production, APP_DEBUG=false, APP_KEY, APP_URL, APP_TIMEZONE |
| Logs | LOG_STACK, LOG_LEVEL |
| Database | DB_CONNECTION, DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD |
| Redis, cache, sessions, queue | REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, CACHE_STORE, SESSION_DRIVER, SESSION_SECURE_COOKIE, QUEUE_CONNECTION |
| Media | MEDIA_DISK, MEDIA_QUEUE, QUEUE_CONVERSIONS_BY_DEFAULT, IMAGE_DRIVER |
| Sales documents on S3 | DOCUMENTS_DISK, AWS_BUCKET, AWS_DEFAULT_REGION, and AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY unless an instance role provides them |
| PDFs | LARAVEL_PDF_CHROME_NO_SANDBOX (DDEV’s container needs it true) |
MAIL_MAILER, MAIL_HOST, MAIL_PORT, MAIL_USERNAME, MAIL_PASSWORD, MAIL_FROM_ADDRESS, MAIL_FROM_NAME. MAIL_MAILER=log keeps mail on the server during rehearsals |
|
| First login | INITIAL_ADMIN_EMAIL, INITIAL_ADMIN_PASSWORD |
| Search | SCOUT_DRIVER, SCOUT_PREFIX, MEILISEARCH_HOST, MEILISEARCH_KEY |
| Websockets | BROADCAST_CONNECTION, REVERB_APP_ID, REVERB_APP_KEY, REVERB_APP_SECRET, REVERB_HOST, REVERB_PORT, REVERB_SCHEME, REVERB_CLIENT_HOST, REVERB_CLIENT_PORT, REVERB_CLIENT_SCHEME |
| Assistant | ASSISTANT_PROVIDER, ASSISTANT_MODEL, and the chosen provider’s key (OPENAI_API_KEY or ANTHROPIC_API_KEY) |
| Forms | FORM_BUILDER_CAPTCHA, TURNSTILE_SITE_KEY, TURNSTILE_SECRET_KEY |
| Sales | SALES_BASE_CURRENCY, EXCHANGE_RATE_API_KEY, SALES_QUOTE_STAFF_EMAIL, SALES_CART_* |
| Supplier feeds | MIDOCEAN_API_KEY, UMBRELLACO_API_TOKEN, XDCONNECTS_*_URL, LALTEX_API_KEY, PFCONCEPT_*_URL, PENCARRIE_API_TOKEN, UNEEK_API_USER, UNEEK_API_PASSWORD, UNEEK_CUSTOMER_NUMBER, RALAWISE_FEED_DIRECTORY, CATALOG_SYNC_TIMEZONE, CATALOG_SKU_PREFIX |
| Legacy import | LEGACY_DB_DATABASE, LEGACY_UPSCALED_SET, LEGACY_IMAGES_DIRECTORY, LEGACY_UPSCALED_DIRECTORY, LEGACY_BLOG_STOCK_DIRECTORY, LEGACY_ATTACHMENTS_DIRECTORY, LEGACY_ATTACHMENTS_DISK |
| Stock photos | PIXABAY_API_KEY |
Each sales file’s row records its own disk, so changing DOCUMENTS_DISK only affects files added
afterwards.
Sizing
Section titled “Sizing”Memory is the limit that binds. Add up the worst cases before raising anything:
- PHP-FPM:
pm.max_childrentimesmemory_limit. - Horizon: 2 workers at up to 2048 MB, 3 at up to 1024 MB, plus the catalog photo pipeline’s ImageMagick, which sits outside PHP’s limit. See Queues and scheduling.
- The import commands, run from the CLI, need a CLI
memory_limitof about 2 GB. - MariaDB’s buffer pool, Redis’s
maxmemoryand Meilisearch.
Uploads are capped by the app at 32 MB (the media library’s max_file_size and Livewire’s temporary
upload rule), whatever PHP and nginx allow.