{"record":{"id":"3695af2b40c402f7","repo":"coollabsio/coolify","slug":"ssh-connection-failed-contextinfo-attemptinfo","errorCode":null,"errorMessage":"SSH connection failed{$contextInfo}{$attemptInfo}","messagePattern":"SSH connection failed(.+?)(.+?)","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"app/Traits/SshRetryable.php","lineNumber":126,"sourceCode":"                // Not retryable or max retries reached\n                break;\n            }\n        }\n\n        // All retries exhausted\n        if ($attempt >= $maxRetries) {\n            Log::error('SSH operation failed after all retries', array_merge($context, [\n                'attempts' => $attempt,\n                'error' => $lastErrorMessage,\n            ]));\n        }\n\n        if ($throwError && $lastError) {\n            // If the error message is empty, provide a more meaningful one\n            if (empty($lastErrorMessage) || trim($lastErrorMessage) === '') {\n                $contextInfo = isset($context['server']) ? \" to server {$context['server']}\" : '';\n                $attemptInfo = $attempt > 1 ? \" after {$attempt} attempts\" : '';\n                throw new \\RuntimeException(\"SSH connection failed{$contextInfo}{$attemptInfo}\", $lastError->getCode());\n            }\n            throw $lastError;\n        }\n\n        return null;\n    }\n}\n","sourceCodeStart":108,"sourceCodeEnd":134,"githubUrl":"https://github.com/coollabsio/coolify/blob/70b9acc42467278373e00de77abb40684e25b395/app/Traits/SshRetryable.php#L108-L134","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before (library code drops the cause: no message, no previous)\nthrow new \\RuntimeException(\"SSH connection failed{$contextInfo}{$attemptInfo}\", $lastError->getCode());\n\n// after (preserve the original exception for diagnosis)\nthrow new \\RuntimeException(\n    \"SSH connection failed{$contextInfo}{$attemptInfo}: \" . ($lastErrorMessage !== '' ? $lastErrorMessage : 'unknown SSH error'),\n    $lastError->getCode(),\n    $lastError\n);","handlingStrategy":"try-catch","validationCode":"use App\\Models\\Server;\n\nfunction sshReachable(Server $server): bool\n{\n    try {\n        $out = instant_remote_process(['echo connection-ok'], $server, false);\n\n        return trim((string) $out) === 'connection-ok';\n    } catch (\\Throwable) {\n        return false;\n    }\n}\n\n// before dispatching long SSH work:\nif (! sshReachable($server)) {\n    // flag server unreachable, notify, and skip instead of burning retries\n}","typeGuard":"function isGenericSshConnectionFailure(\\Throwable $e): bool\n{\n    return $e instanceof \\RuntimeException\n        && str_starts_with($e->getMessage(), 'SSH connection failed');\n}","tryCatchPattern":"try {\n    $this->executeWithSshRetry(fn () => $callback(), ['server' => $server->name], true);\n} catch (\\RuntimeException $e) {\n    if (isGenericSshConnectionFailure($e)) {\n        // empty-message failure: the real cause was swallowed — log context and surface a clear, actionable error\n        Log::error('SSH unreachable', ['server' => $server->id, 'code' => $e->getCode()]);\n        // mark the server as unreachable / queue a later re-attempt instead of failing hard\n    } else {\n        throw $e; // meaningful errors keep their original message\n    }\n}","preventionTips":["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."],"tags":["ssh","network","retry","connection","remote-process"],"backgroundTag":null,"analyzedSha":"70b9acc42467278373e00de77abb40684e25b395","analyzedAt":"2026-08-17T01:41:01.313Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}