Environment
- Image: nfrastack/freescout:latest (image 2.1.3, FreeScout 1.8.226)
- SCHEDULER_TYPE=CRON (default); the ALT mode is affected as well (see below)
Summary
FreeScout’s artisan schedule:run intentionally spawns a long-running queue:work daemon (its own worker-supervision logic; the worker typically lives ~10 minutes before being cycled). The cron entry generated by the image wraps schedule:run in a shell pipeline:
* * * * * { echo "...Scheduling start"; sudo -u nginx TZ=... php /www/html/artisan schedule:run | awk '{ ... }'; echo "...Scheduling Finished"; } | tee -a /logs/laravel/scheduler.log
The spawned queue:work daemon inherits stdout connected to the awk/tee pipe, so the pipeline does not complete until the worker exits (~10 minutes). Since job runs are serialized, the effective scheduler cadence becomes one schedule:run per ~10 minutes instead of one per minute.
The most painful consequence: freescout:fetch-emails is scheduled every minute, but incoming mail is actually fetched only every ~10 minutes.
Evidence
scheduler.log from a stock container (note the gap between “start” and “Finished”, and no cycles in between):
2026-07-13.06:00:03 Scheduling start
2026-07-13 06:00:04 Running scheduled command: ... freescout:fetch-emails ...
2026-07-13 06:00:06 Running scheduled command: ... queue:work --queue='emails,default,...' --sleep=5 --tries=1 --timeout=1800 ...
2026-07-13.06:09:33 Scheduling Finished
2026-07-13.06:10:00 Scheduling start
Laravel log also shows workers being SIGTERM’ed by FreeScout’s duplicate-worker cleanup when cycles overlap after cache clears:
production.ERROR: The process has been signaled with signal "15".
... Illuminate\Console\Scheduling\Event->runCommandInForeground(...)
ALT scheduler is affected too
/container/run/available/41-freescout-scheduler/run has the same construction inside a sequential loop:
while true; do
echo "$(_ts) Scheduling Starting"
sudo -u "${NGINX_USER}" TZ="${TIMEZONE}" php "${NGINX_WEBROOT%/}"/artisan schedule:run | \
TZ="${TIMEZONE}" awk '{ print strftime("%Y-%m-%d.%H:%M:%S"), $0; fflush() }'
echo "$(_ts) Scheduling Finished"
_sleep_until_next_minute
done
Suggested fixes
- Don’t let the wrapper wait on schedule:run’s descendants: redirect output to the log file directly (losing the awk timestamps or using a non-blocking stamper), e.g. php artisan schedule:run >> "$log" 2>&1, so the inherited-stdout pipe cannot hold the cycle open; or
- Run schedule:run --no-interaction: FreeScout skips launching its internal queue:work daemon when -n is passed — combined with a dedicated worker service this gives clean 1-minute cycles. Note: the existing 30-laravel-worker service runs plain php artisan queue:work, which listens only to the default queue; FreeScout dispatches to emails,default,{instance-identifier} (see config('app.queue_work_params') and Helper::getWorkerIdentifier()), so the service must pass those queue parameters, otherwise outgoing-mail jobs are never processed.
History — this is a regression of #199
The same symptom was reported for the tiredofit-era image in #199 (“Freescout cronjob delayed for 10 min after container start”; users saw mail fetch delays from 10 minutes up to 8–10 hours). The mechanics were identified there: schedule:run keeps FreeScout’s queue:work daemon in its process group, and busybox crond skips subsequent runs while the previous one is still “running”. A workaround env EXPERIMENTAL_CRON_BACKGROUND=TRUE was added in 1.17.114.
That variable does not exist in the current 2.x codebase (verified inside image 2.1.3: no CRON_BACKGROUND/EXPERIMENTAL handling in /container/defaults or /container/functions), and the new | awk | tee log pipeline re-introduces the blocking — now on every cycle, not just container start. Please consider reinstating a supported detach/background option for the scheduler.
Workaround we currently use
A host-level systemd timer runs docker exec --user nginx php artisan schedule:run --no-interaction every minute; the container’s own cron remains in place solely to supervise queue:work. Laravel’s withoutOverlapping mutexes (shared application cache) prevent duplicate task runs between the two schedulers.
Environment
Summary
FreeScout’s artisan schedule:run intentionally spawns a long-running queue:work daemon (its own worker-supervision logic; the worker typically lives ~10 minutes before being cycled). The cron entry generated by the image wraps schedule:run in a shell pipeline:
* * * * * { echo "...Scheduling start"; sudo -u nginx TZ=... php /www/html/artisan schedule:run | awk '{ ... }'; echo "...Scheduling Finished"; } | tee -a /logs/laravel/scheduler.logThe spawned queue:work daemon inherits stdout connected to the awk/tee pipe, so the pipeline does not complete until the worker exits (~10 minutes). Since job runs are serialized, the effective scheduler cadence becomes one schedule:run per ~10 minutes instead of one per minute.
The most painful consequence: freescout:fetch-emails is scheduled every minute, but incoming mail is actually fetched only every ~10 minutes.
Evidence
scheduler.log from a stock container (note the gap between “start” and “Finished”, and no cycles in between):
Laravel log also shows workers being SIGTERM’ed by FreeScout’s duplicate-worker cleanup when cycles overlap after cache clears:
ALT scheduler is affected too
/container/run/available/41-freescout-scheduler/run has the same construction inside a sequential loop:
Suggested fixes
History — this is a regression of #199
The same symptom was reported for the tiredofit-era image in #199 (“Freescout cronjob delayed for 10 min after container start”; users saw mail fetch delays from 10 minutes up to 8–10 hours). The mechanics were identified there: schedule:run keeps FreeScout’s queue:work daemon in its process group, and busybox crond skips subsequent runs while the previous one is still “running”. A workaround env EXPERIMENTAL_CRON_BACKGROUND=TRUE was added in 1.17.114.
That variable does not exist in the current 2.x codebase (verified inside image 2.1.3: no CRON_BACKGROUND/EXPERIMENTAL handling in /container/defaults or /container/functions), and the new | awk | tee log pipeline re-introduces the blocking — now on every cycle, not just container start. Please consider reinstating a supported detach/background option for the scheduler.
Workaround we currently use
A host-level systemd timer runs docker exec --user nginx php artisan schedule:run --no-interaction every minute; the container’s own cron remains in place solely to supervise queue:work. Laravel’s withoutOverlapping mutexes (shared application cache) prevent duplicate task runs between the two schedulers.