Deploying on Forge
This page covers what a Textstem site needs on a Laravel Forge server beyond a standard Laravel deploy, and the problems that are easy to hit.
PHP version
Use the PHP version your composer.lock needs for both the site and the command line. Forge runs your deploy script's $FORGE_COMPOSER with the site's PHP version, but a plain php uses the server's CLI default, which can be older. When they differ, Composer installs fine and then every php artisan command fails with:
Composer detected issues in your platform: Your Composer dependencies require a PHP version ">= 8.4.1". You are running 8.3.x.
To fix it:
- In the deploy script, call
$FORGE_PHP artisan …rather thanphp artisan …, so it always matches the site. - In Forge, make the same version the CLI default (Server › PHP › Make CLI default), so artisan commands you run over SSH and in scheduled jobs match too.
Deploy script
set -x
exec 1>&2
cd /home/forge/example.com
$FORGE_PHP artisan down
git pull origin $FORGE_SITE_BRANCH
npm ci
npm run build
$FORGE_COMPOSER install --no-dev --no-interaction --prefer-dist --optimize-autoloader
$FORGE_PHP artisan optimize:clear
$FORGE_PHP artisan migrate --force
$FORGE_PHP artisan textstem:refresh-assets
$FORGE_PHP artisan config:cache
$FORGE_PHP artisan route:cache
$FORGE_PHP artisan view:cache
$FORGE_PHP artisan queue:restart
$FORGE_PHP artisan up
Notes:
- Clear before you cache.
optimize:clearremoves the config, route and view caches, so it must run before the:cachecommands, not after them. npm ciis needed even if you build assets elsewhere: live page screenshots loadpuppeteerfromnode_modules.queue:restartmakes running workers pick up the new code.set -xprints each command in the deployment log, so a failure shows which line caused it.exec 1>&2sends all command output to the log's error stream. On one Forge server, deploys failed straight after Composer with/dev/fd/63: line 74: INFO: command not found, as if artisan'sINFOoutput was being run as a command; this line fixed it.
Queue worker
Textstem runs AI generation, asset processing, SEO and accessibility analysis and screenshots on the queue, so the site needs a worker (Site › Queue in Forge). Set it up with:
| Setting | Value |
|---|---|
| Connection | The same as QUEUE_CONNECTION in the site's environment, such as redis or database |
| Queue | default, or the queues you've configured |
| PHP version | The site's version |
| Timeout | At least 120 seconds; screenshot jobs allow themselves 120 |
A worker on the wrong connection never sees the jobs: they stay delayed or pending in the admin's queue monitor. Check which connection a running worker uses with:
ps aux | grep '[q]ueue:work'
If several sites on the server share one Redis, give each its own REDIS_PREFIX in its environment. Laravel otherwise builds the prefix from APP_NAME, and two sites with the same name would take each other's jobs.
See Queues for what runs on the queue.
Scheduler
Turn on Laravel's scheduler in Forge (Server › Scheduler) with php artisan schedule:run every minute, using the site's PHP version. Textstem registers its own housekeeping tasks; see Scheduled Tasks.
Live page screenshots
Screenshots need Node, puppeteer and a headless Chrome on the server; see Live page screenshots. Check the setup as the forge user with:
php artisan textstem:page-screenshots --check
"No usable sandbox!"
Ubuntu 23.10 and later, including Forge's current servers, use AppArmor to block the user namespaces Chrome's sandbox needs. The check then fails with:
FATAL:zygote_host_impl_linux.cc No usable sandbox! If you are running on Ubuntu 23.10+ ...
Running as forge (not root) is not enough on its own. Allow Puppeteer's browsers to use namespaces with an AppArmor profile. Puppeteer installs two browsers, and the screenshots use chrome-headless-shell, so the profile covers both:
sudo nano /etc/apparmor.d/puppeteer
abi <abi/4.0>,
include <tunables/global>
profile puppeteer-chrome /home/forge/.cache/puppeteer/chrome/*/chrome-linux64/chrome flags=(unconfined) {
userns,
}
profile puppeteer-headless-shell /home/forge/.cache/puppeteer/chrome-headless-shell/*/chrome-headless-shell-linux64/chrome-headless-shell flags=(unconfined) {
userns,
}
Then load it and check again:
sudo apparmor_parser -r /etc/apparmor.d/puppeteer
php artisan textstem:page-screenshots --check
The * in each path matches any browser version, so the profile keeps working after Puppeteer updates. Type the profile into nano rather than pasting it if your terminal adds characters such as │ when copying: AppArmor rejects them.
The alternative is TEXTSTEM_SCREENSHOT_NO_SANDBOX=true, which turns Chrome's sandbox off. That's acceptable when the browser only ever loads your own pages, but the AppArmor profile keeps the sandbox on.
After changing screenshot settings, run php artisan config:cache and php artisan queue:restart.
Checklist
- [ ] Site PHP and CLI PHP both match
composer.lock - [ ] The deploy script uses
$FORGE_PHP, clears before caching and restarts the queue - [ ] A queue worker runs on the same connection as
QUEUE_CONNECTION - [ ] The scheduler runs every minute
- [ ]
php artisan textstem:health-checkpasses - [ ] If screenshots are on,
php artisan textstem:page-screenshots --checkends with "Test screenshot taken"