phanan/remote
Run Artisan commands on remote servers over SSH directly from your local Laravel app. Configure connections, then execute and inspect them as if they were local—handy for deployment, maintenance, and managing multiple environments. No extra tooling required.
phanan/remote is a thin Laravel 5 wrapper around phpseclib that gives you a Remote facade for SFTP transfers and SSH command execution. It is deliberately small — one connection abstraction, a handful of file operations — so it fits best as a plumbing dependency inside deploy scripts, sync commands, and queue jobs rather than as a first-class filesystem driver.
Install it and wire it up:
composer require phanan/remote
php artisan vendor:publish --provider="Phanan\Remote\RemoteServiceProvider"
On Laravel 5.5+ the provider is auto-discovered. On 5.0–5.4 register Phanan\Remote\RemoteServiceProvider in config/app.php and add the Remote alias pointing at the package's facade class. Then edit the published config/remote.php with host, port, username, password (or key path), root directory, and timeout.
Look first at two files in vendor/phanan/remote: the facade (it lists every public method and its exact signature) and the main service class (it shows how phpseclib's SFTP and SSH2 objects are constructed and cached). Reading those two files takes five minutes and removes all guesswork about the API.
The canonical first use case: a php artisan remote:push command that uploads a locally built asset directory to a staging server.
Remote::connect();
Remote::put(base_path('public/build'), '/var/www/app/public/build');
Remote::disconnect();
Treat it as a transport layer, not a filesystem. The natural pattern is to keep Laravel's Storage disk for local work, then push the finished artifact over the remote connection. Generate the file locally, verify it, then put() it. This keeps your code testable — you can fake the local disk and only the final transfer touches the network.
Centralize the connection lifecycle. Open and close explicitly around a batch of operations rather than relying on lazy connection:
Remote::connect();
try {
foreach ($files as $local => $remote) {
Remote::put($local, $remote);
}
Remote::run('cd /var/www/app && php artisan cache:clear');
} finally {
Remote::disconnect();
}
This matters in queue workers, where the process outlives a single job and a stale handle will bite you on the next job.
Wrap it in a dedicated service class. Don't sprinkle Remote:: calls through controllers. A RemoteDeployer or RemoteSync class with push(), pull(), exists(), and run() methods gives you one place to swap in a fake for tests and one place to add retry logic.
Use SSH execution for post-transfer steps. The real value in day-to-day work is the combination: upload a release archive, then run tar -xzf and composer install --no-dev over SSH in the same session. That turns a two-step manual deploy into one command.
Pull remote artifacts for debugging. get() on /var/log/app/error.log or a remote storage directory into storage/app/remote/ is a genuinely useful debugging pattern when you have no shell access to the box. Schedule it as a command if you need periodic log collection.
Layer multiple named connections if the config supports it. If you deploy to staging and production, keep separate config entries and pass the connection name to connect(). Verify in the source whether the package supports named connections or a single global config — if it is single-connection, hold two config arrays in your own service class rather than mutating the package config at runtime.
The package is archived. No bug fixes, no PHP 8.x or Laravel 9+ compatibility work. Pin the version in composer.json, and if you are starting fresh, use Laravel's native SFTP filesystem driver or league/flysystem-sftp-v3 instead — you get the full Storage API, better error handling, and active maintenance. Choose phanan/remote only if you are already on Laravel 5 and need SSH command execution alongside transfers.
Config caching will silently ignore your edits. If you run php artisan config:cache in deployment, changes to config/remote.php won't take effect until you re-cache. This is the single most common "why isn't it connecting to the new host" cause.
Long-running processes hold stale connections. Queue workers and Octane-style runtimes keep the phpseclib handle alive across jobs. Always disconnect() in a finally block, or reconnect at the top of each job. Remote servers and firewalls will drop idle connections and the next call fails cryptically.
Path resolution depends on the configured root. A leading slash may mean "absolute on the server" or "relative to the configured root directory" depending on how the wrapper joins paths. Test with a known path and confirm with exists() before relying on it in a batch operation.
Errors are not always exceptions. Some methods return booleans, others may throw phpseclib exceptions. Wrap calls defensively and check return values rather than assuming an exception will surface. Log the failing path — phpseclib's own messages are terse.
Large files and timeouts. The default timeout in the config is likely too short for a multi-hundred-megabyte upload. Raise it, and consider chunking or using a tarball over SSH for anything substantial rather than moving many small files individually.
phpseclib version sensitivity. The package's composer constraint pins an older phpseclib. If another dependency wants a newer major version, you get a conflict. Check composer why phpseclib/phpseclib before adding anything else that does SSH.
Extension point if you need to fork. The service provider binds a single connection class; subclass it and rebind in your own provider to add retries, logging, or connection pooling. That is a cleaner path than patching vendor/.
How can I help you explore Laravel packages today?