Skip to content

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.

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

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:

Terminal window
# 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-only
composer install --no-dev --optimize-autoloader --no-interaction
# 3. Assets: the admin, then one build per theme.
npm ci
npm run build
npm run build:default
npm run build:gopromo
# 4. Schema and roles.
php artisan migrate --force
php artisan roles:sync
# 5. Caches.
php artisan route:cache
php artisan view:cache
php artisan event:cache
php 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 finish
php 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:sync on every deploy. See Authorization. It fails if a role lists a capability that no module registers.
  • Don’t run config:cache. A closure in config/filament-flexible-content-blocks.php (seo_image) can’t be serialized, so the command fails.
  • --no-dev means no Faker, so nothing a production seeder runs may use a factory.

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

After the steps above, on an empty database:

  1. php artisan storage:link.
  2. Set INITIAL_ADMIN_EMAIL and INITIAL_ADMIN_PASSWORD, and seed the first admin: php artisan db:seed --class=AdminUserSeeder, or run LegacySeeder if you import. With no password set, it prints a random one.
  3. For a first import of a feed: catalog:add-supplier <source> queues its new products as drafts by the category mappings, and catalog:publish-supplier <source> publishes them. Follow with cms:reindex and catalog:search-rebuild, since publishing in bulk skips observers.
  4. At cutover, move the number sequences above the legacy ones with sales:sequence <type> --next=<number>.

Details are in Commands.

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 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.

Memory is the limit that binds. Add up the worst cases before raising anything:

  • PHP-FPM: pm.max_children times memory_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_limit of about 2 GB.
  • MariaDB’s buffer pool, Redis’s maxmemory and 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.