Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Remote Laravel Package

laravelcollective/remote

LaravelCollective Remote brings SSH and SFTP to Laravel. Connect to remote servers, run commands, upload and download files, and manage multiple named connections through a clean, familiar Laravel-style API, ideal for deployments, server maintenance, and remote automation.

View on GitHub
Deep Wiki
Context7

Getting Started

laravelcollective/remote is a thin, fluent wrapper around phpseclib that gives you SSH::into('prod')->run(...) from anywhere in a Laravel app. It is not a deployment tool — it is a runtime SSH/SFTP client you call from Artisan commands, jobs, and services.

  1. Install and check the constraint first. composer require laravelcollective/remote:^6.2. The last release is from 2020 and caps at illuminate/support ~5.0|~6.0|~7.0|~8.0, so on Laravel 9+ composer require will fail resolution. Verify with composer why-not laravel/framework ^10.0 before you build anything on top of it; if you're on a newer framework you'll need a fork with widened constraints (or to skip the package and use phpseclib directly).
  2. Publish the config. php artisan vendor:publish --provider="LaravelCollective\Remote\RemoteServiceProvider" --tag=config gives you config/remote.php. Everything the package does is driven from this file — read it before writing code.
  3. Shape the config around env vars, never literals:
// config/remote.php
return [
    'default' => env('REMOTE_CONNECTION', 'production'),

    'connections' => [
        'production' => [
            'host'      => env('REMOTE_PROD_HOST'),
            'username'  => env('REMOTE_PROD_USER'),
            'password'  => env('REMOTE_PROD_PASSWORD', ''),
            'key'       => env('REMOTE_PROD_KEY_PATH', ''),   // path to private key
            'keytext'   => '',                                // or the key body itself
            'keyphrase' => env('REMOTE_PROD_KEY_PASSPHRASE', ''),
            'agent'     => '',
            'timeout'   => 30,
        ],
    ],
];
  1. First use case: one read-only smoke test in Tinker.
$code = SSH::into('production')->run('uname -a', function ($line) {
    dump(rtrim($line, "\r\n"));
});
dump($code); // exit status, not output

If that returns 0 and prints the kernel line, provider registration, the SSH alias, auth, and the config are all sound.

  1. Where to look first. RemoteManager (into(), connection(), default connection, extend()), then Connection (run, status, put, get, putString, getString, define). Those two classes are the whole package.

Implementation Patterns

Wrap it behind your own interface. The package's surface is small but its return shapes and exceptions are not yours to control. Define one narrow contract, bind it, and inject it:

interface RemoteShell
{
    public function run(string $command, ?callable $onLine = null): int;
    public function put(string $local, string $remote): bool;
    public function get(string $remote, string $local): bool;
}

final class SshRemoteShell implements RemoteShell
{
    public function __construct(private string $connection) {}

    public function run(string $command, ?callable $onLine = null): int
    {
        return SSH::into($this->connection)->run($command, $onLine);
    }
    // ...
}

Bind in AppServiceProvider, fake in tests, and you've isolated yourself from a package that may not see another release.

Stream output into Artisan commands. The callback is the only way to see stdout. Accumulate it, strip \r\n, and print it live so long migrations don't look hung:

$code = SSH::into('prod')->run(
    'cd /var/www/app && php artisan migrate --force 2>&1',
    fn ($line) => $this->line(rtrim($line, "\r\n"))
);

return $code === 0 ? self::SUCCESS : self::FAILURE;

Chain commands into one string. State does not survive between calls (see Gotchas). Any multi-step remote operation should be a single run() with &&:

SSH::into('prod')->run(
    'set -e; cd /var/www/app && php artisan down && git pull && composer install --no-dev -o && php artisan migrate --force && php artisan up'
);

Fan out across connections and collect exit codes. Treat failures as data, not exceptions:

$results = collect(config('remote.connections'))
    ->keys()
    ->mapWithKeys(fn ($name) => [$name => SSH::into($name)->run('systemctl is-active nginx')]);

$failed = $results->filter(fn ($code) => $code !== 0);

Use define() for runtime connections (tenant servers, user-supplied hosts) rather than mutating config() at runtime — the manager memoises by name:

SSH::into($alias)->define($alias, [
    'host' => $host, 'username' => $user, 'keytext' => $key, 'timeout' => 15,
]);
SSH::into($alias)->put($localPath, '/var/www/html/release.tar.gz');

File transfers go through put/get/getString/putString. Write generated config or payloads with putString instead of a temp file round-trip. Always check the returned bool — SFTP failures are silent otherwise.

Queue it, but don't serialise it. Remote work belongs in a job, but a Connection holds an open socket and will not serialise. Pass a connection name and re-resolve inside handle(). Set the job's $timeout above the remote command's worst case, and give the connection its own generous timeout too.

Test at your own boundary. Bind a fake RemoteShell in the container for unit tests; add one integration test gated by env (a throwaway linuxserver/openssh-server container or staging) that exercises a real connect, run, put, and get. Tag it @group remote so CI doesn't run it on every commit.

Escape anything interpolated into a command. The remote shell applies the same quoting rules as a POSIX local shell, so escapeshellarg($userInput) is correct and necessary. Better: validate against an allowlist and never interpolate request data at all.

Gotchas and Tips

run() returns the exit code, not the output. This is the single most common mistake. If you need output, pass a callback; if you need both, capture in the callback and inspect the return value.

Nothing persists between run() calls. phpseclib's exec opens a fresh channel per call, so cd, exported env vars, and shell functions from one call are gone in the next. Use cd x && cmd inside a single string.

Long-running commands silently truncate. The connection timeout doubles as the read timeout; if a command outlives it you get partial output and a misleading exit code. Bump timeout per connection for migrations/composer installs (60–300s), and never run interactive commands — a sudo password prompt will hang the request until PHP's max_execution_time kills it. Use sudo -n with NOPASSWD entries, or -o BatchMode=yes for git.

Background processes keep the channel open. cmd & still waits for the channel to close. Redirect and detach: nohup cmd > /tmp/cmd.log 2>&1 &. Consider status()/exit-code retrieval an extra round trip that this pattern can stall.

Output arrives with \r\n and possibly ANSI escapes. rtrim($line, "\r\n") before logging; strip escape sequences before storing in a DB column or email.

Host key verification is off by default. phpseclib will accept whatever key the host presents. On untrusted networks, pin the fingerprint (getGateway()->getServerPublicHostKey() and compare) — otherwise you have a MITM-shaped hole in your deploy path.

SFTP paths. Relative paths resolve against the login user's home directory, and ~ isn't expanded. Use absolute paths. put() to a non-existent directory returns false with no exception — mkdir -p over run() first, or check the bool and throw.

The manager caches connections by name. In queue workers and Octane that means a stale socket survives across jobs and requests; the symptom is "connection reset"/timeouts on the second job of a worker. Rebind the manager in a job middleware, or create a uniquely-named connection per job via define(). A fresh Connection per unit of work is the safest habit.

keytext in .env is a trap. Multi-line PEM values get mangled by dotenv and by config:cache snapshots. Store the key as a file written at deploy time (chmod 600, path in key), or base64-encode the key in env and decode it in the config file.

Cached config turns env() into null. If credentials work locally but not in prod, run php artisan config:clear before you open a debugger — calling env() outside a config file under a cached config is the classic Laravel production-only failure.

Laravel 9+ will not install. The illuminate/* constraint stops at ~8.0 and there has been no release since 2020. Your options, in order of preference: (1) implement the small run/put/get surface directly against phpseclib — it's ~100 lines and removes the dead dependency; (2) fork and widen the constraint, accepting maintenance; (3) use spatie/ssh for command execution. Do not --ignore-platform-reqs your way past it; the constraint reflects an untested framework.

If the SSH alias is missing, check extra.laravel in the package's composer.json and run php artisan package:discover -v followed by composer dump-autoload. Don't add a manual alias until you know why discovery failed.

Extension points before forking: rebind the manager or your interface in your own provider, subclass Connection and register it via RemoteManager::extend(), and keep the facade confined to your adapter class. Never patch vendor/ — the fix vanishes on the next composer update and never appears in review.

Log what you ran. Wrap your adapter to record connection name, redacted command, exit code, and duration. When a remote deploy half-fails at 3am, the exit code per step is worth more than any stack trace.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin