TL;DR If a web request shells out to a slow CLI (docker, kubectl, rsync…), move it to a queued job — PHP-FPM/nginx will kill it at ~30s anyway. Processes spawned from an HTTP worker inherit almost no environment: no HOME, a bare PATH. Tools that read ~/.docker or ~/.kube silently fail. Inject the env explicitly. Wrap the shell-out in a driver/contract so you can fake it in tests instead of running real docker in CI. The setup I have an app that provisions infrastructure by calling command-line tools — docker here, kubectl there. Classic "orchestrate the CLI from PHP" situation. It worked perfectly from php artisan tinker on my machine and fell apart the moment it ran from a browser request. Two separate reasons, both worth knowing. Gotcha 1: the web request is the wrong place Provisioning takes 40–90 seconds. A web request does not have that long. nginx/PHP-FPM cut you off around 30s (max_execution_time, fastcgi_read_timeout), the browser sees a 504, and — worse — you don't actually know if the work finished or died halfway. The fix is boring and correct: the controller validates, dispatches a job, and returns immediately. The long-running shell-out happens on the queue where it can take minutes and retry cleanly. // Controller/Livewire action — return fast ProvisionDeploymentJob::dispatch($deployment); return back()->with('status', 'Provisioning started'); Enter fullscreen mode Exit fullscreen mode The job owns the slow part, reports progress, and is the only place a real CLI ever runs. Gotcha 2: HTTP workers have no HOME This one cost me an afternoon. The queued job still failed — but only under the web-triggered worker, never from tinker. The error was docker and kubectl behaving as if they had no config: no registry auth, no cluster context. The reason: a process spawned from an HTTP/FPM context inherits a stripped environment. HOME is often unset, and PATH is minimal. Both docker and kubectl resolve their config relative to HOME (~/.docker/config.json, ~/.kube/config). No HOME → they look in the wrong place → they act unauthenticated. Symfony Process does not inherit your shell's env for you. So inject what the child needs: use Symfony\Component\Process\Process; $process = new Process($command, $workingDir, [ 'HOME' => $this->home, // so docker/kubectl find their config 'PATH' => $this->path, // so the binary is resolvable at all 'KUBECONFIG' => $this->kubeconfig, ]); Enter fullscreen mode Exit fullscreen mode The variables that actually matter when spawning these CLIs: Env var Why it matters Symptom if missing HOME Root for ~/.docker, ~/.kube Tool runs unauthenticated / no context PATH Locate the binary "command not found" KUBECONFIG Explicit kube context Falls back to default/none Make it testable Running real docker in CI is a non-starter. Put the shell-out behind a contract and resolve a fake in tests — the same driver-based pattern you'd use for any swappable backend. interface WorkloadRuntimeContract { public function start(Deployment $deployment): void; } Enter fullscreen mode Exit fullscreen mode it('provisions without touching a real CLI', function () { $this->app->bind(WorkloadRuntimeContract::class, FakeWorkloadRuntime::class); ProvisionDeploymentJob::dispatch($deployment); expect(app(WorkloadRuntimeContract::class)->started) ->toContain($deployment->id); }); Enter fullscreen mode Exit fullscreen mode Takeaway Two rules I'd tape to the wall: long CLI work belongs on the queue, not the request; and a process you spawn starts with an empty environment unless you fill it. The HOME-is-unset one is nasty because it only shows up in the HTTP path — the exact place that's hardest to debug. Give your child processes an explicit env and a fake for tests, and the whole thing stops being spooky.
Two gotchas when your Laravel app shells out to a CLI
Full Article
Original Source
Read the full article at Dev →KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.