coollabsio/coolify · error · RuntimeException
SSH connection failed{$contextInfo}{$attemptInfo}
Error message
SSH connection failed{$contextInfo}{$attemptInfo} What it means
The SshRetryable trait wraps SSH callbacks in a retry loop (configurable via constants.ssh.max_retries with exponential backoff), re-attempting only when the error text matches known retryable SSH patterns (connection reset/refused/timeout, auth failures, etc.). After the loop, if throwError is true and the last caught Throwable has a completely empty message, it throws this generic RuntimeException ('SSH connection failed[ to server X][ after N attempts]') carrying only the original error code. It is a last-resort message for failures where the underlying exception said nothing at all.
Source
Thrown at app/Traits/SshRetryable.php:126
// Not retryable or max retries reached
break;
}
}
// All retries exhausted
if ($attempt >= $maxRetries) {
Log::error('SSH operation failed after all retries', array_merge($context, [
'attempts' => $attempt,
'error' => $lastErrorMessage,
]));
}
if ($throwError && $lastError) {
// If the error message is empty, provide a more meaningful one
if (empty($lastErrorMessage) || trim($lastErrorMessage) === '') {
$contextInfo = isset($context['server']) ? " to server {$context['server']}" : '';
$attemptInfo = $attempt > 1 ? " after {$attempt} attempts" : '';
throw new \RuntimeException("SSH connection failed{$contextInfo}{$attemptInfo}", $lastError->getCode());
}
throw $lastError;
}
return null;
}
}
View on GitHub (pinned to 70b9acc424)
Solutions
- Verify the network path from the Coolify host to the target server on port 22: nc -vz <host> 22 / ping, and check firewall/security-group rules.
- Reproduce manually with verbose SSH from the Coolify host (ssh -vvv ...) to surface the real failure the empty message hid.
- Check the Laravel log for 'SSH operation failed after all retries' (it includes the context array with server/command) and correlate with the timestamp.
- Inspect sshd-side limits on the target: MaxStartups, fail2ban bans, MaxAuthTries — bursts of parallel Coolify SSH commands frequently trip connection resets.
- For flaky links, raise retry tuning in config (constants.ssh.max_retries, retry_base_delay, retry_max_delay) so transient resets are absorbed.
- If you control the calling code, ensure exceptions thrown inside the callback always carry a message so the generic fallback (which drops $lastError) is never used.
Example fix
// before (library code drops the cause: no message, no previous)
throw new \RuntimeException("SSH connection failed{$contextInfo}{$attemptInfo}", $lastError->getCode());
// after (preserve the original exception for diagnosis)
throw new \RuntimeException(
"SSH connection failed{$contextInfo}{$attemptInfo}: " . ($lastErrorMessage !== '' ? $lastErrorMessage : 'unknown SSH error'),
$lastError->getCode(),
$lastError
); Defensive patterns
Strategy: try-catch
Validate before calling
use App\Models\Server;
function sshReachable(Server $server): bool
{
try {
$out = instant_remote_process(['echo connection-ok'], $server, false);
return trim((string) $out) === 'connection-ok';
} catch (\Throwable) {
return false;
}
}
// before dispatching long SSH work:
if (! sshReachable($server)) {
// flag server unreachable, notify, and skip instead of burning retries
} Type guard
function isGenericSshConnectionFailure(\Throwable $e): bool
{
return $e instanceof \RuntimeException
&& str_starts_with($e->getMessage(), 'SSH connection failed');
} Try / catch
try {
$this->executeWithSshRetry(fn () => $callback(), ['server' => $server->name], true);
} catch (\RuntimeException $e) {
if (isGenericSshConnectionFailure($e)) {
// empty-message failure: the real cause was swallowed — log context and surface a clear, actionable error
Log::error('SSH unreachable', ['server' => $server->id, 'code' => $e->getCode()]);
// mark the server as unreachable / queue a later re-attempt instead of failing hard
} else {
throw $e; // meaningful errors keep their original message
}
} Prevention
- Run a cheap pre-flight SSH echo check before dispatching deployments or batched commands to a server.
- Keep sshd MaxStartups, MaxAuthTries, and fail2ban thresholds generous enough for Coolify's parallel SSH bursts.
- Tune constants.ssh.max_retries and backoff delays to your network quality so transient resets are absorbed silently.
- Always pass context (['server' => ..., 'command' => ...]) to executeWithSshRetry — with empty messages, context is the only diagnostic left.
- Ensure any exception you throw inside the retry callback carries a non-empty message so the generic fallback path never triggers.
When it happens
Trigger: executeWithSshRetry($callback, $context, true) throws this when the callback's exception message is empty ('' or whitespace): an empty message never matches isRetryableSshError(), so the loop breaks immediately (typically attempt 0, hence no 'after N attempts' suffix and no 'SSH operation failed after all retries' log entry). Common producers: SSH/process failures that exit with code 255 but write nothing to stderr, network drops where the SSH process dies silently, or any wrapped call (e.g. instant_remote_process / remote_process) raising an exception constructed without a message. The 'to server {name}' suffix appears only when $context['server'] was passed.
Common situations: Firewalls or fail2ban silently dropping packets so ssh exits with no output; sshd MaxStartups exceeded during parallel Coolify deployments (connections reset before banner); servers rebooting or DNS resolving but routing black-holed; PHP/dependency upgrades where a Process exception is created with an empty message; Docker network hiccups on the Coolify host. Because the original message is empty and this RuntimeException is not chained to $lastError as previous, the root cause is invisible except via the context logged at failure time.
Related errors
- Private key not found. Please add a private key to the appli
- Failed to read Git source. Please verify repository access a
- Failed to read the Docker Compose file from the repository.
- Command execution failed (exit code {$process_result->exitCo
- Server OS type is not supported for automated installation.
AI-assisted analysis of coollabsio/coolify@70b9acc424 (2026-08-17).
Data as JSON: /api/errors/3695af2b40c402f7.
Report an issue: GitHub.