#!/usr/bin/env node /** * Fetch a Coolify app's FULL container logs via SSH + `docker logs`. * * More powerful than the REST API path (scripts/fetch-app-logs.mjs): it includes * timestamps, supports a `--since` date filter, and works for both dockerfile and * dockercompose apps. Use this when the REST `/logs` endpoint returns little or * nothing (e.g. quiet services, or compose apps Coolify can't tail via the API). * * Usage (from the vibn-frontend/ directory): * node --env-file=.env.local scripts/fetch-app-logs-ssh.mjs [sinceISO] [tail] * * Examples: * # Everything since the start of today (UTC) * node --env-file=.env.local scripts/fetch-app-logs-ssh.mjs hou4vy5mtyg5mrx3w4nl2lxv 2026-06-12 * # Last 500 lines, no date filter * node --env-file=.env.local scripts/fetch-app-logs-ssh.mjs hou4vy5mtyg5mrx3w4nl2lxv "" 500 * * The is the last path segment of the Coolify app URL. * Env (from vibn-frontend/.env.local): * COOLIFY_SSH_HOST, COOLIFY_SSH_PORT, COOLIFY_SSH_USER, COOLIFY_SSH_PRIVATE_KEY_B64 */ import ssh2 from "ssh2"; const { Client } = ssh2; const uuid = process.argv[2]; const since = process.argv[3] || ""; // optional ISO date, e.g. 2026-06-12 const tail = process.argv[4] || "2000"; if (!uuid) { console.error( "Usage: node --env-file=.env.local scripts/fetch-app-logs-ssh.mjs [sinceISO] [tail]", ); process.exit(1); } const keyB64 = process.env.COOLIFY_SSH_PRIVATE_KEY_B64; if (!process.env.COOLIFY_SSH_HOST || !keyB64) { console.error("Missing COOLIFY_SSH_HOST / COOLIFY_SSH_PRIVATE_KEY_B64 in .env.local"); process.exit(1); } const cfg = { host: process.env.COOLIFY_SSH_HOST, port: Number(process.env.COOLIFY_SSH_PORT ?? 22), username: process.env.COOLIFY_SSH_USER ?? "vibn-logs", privateKey: Buffer.from(keyB64, "base64").toString("utf8"), readyTimeout: 8000, }; function runRemote(cmd) { return new Promise((resolve, reject) => { const conn = new Client(); let out = ""; let errOut = ""; conn .on("ready", () => { conn.exec(cmd, (err, stream) => { if (err) { conn.end(); return reject(err); } stream .on("close", (code) => { conn.end(); resolve({ code, out, errOut }); }) .on("data", (d) => (out += d.toString())) .stderr.on("data", (d) => (errOut += d.toString())); }); }) .on("error", reject) .connect(cfg); }); } // Resolve the container by name (Coolify names them with the app UUID), then // dump its logs. The $(...) here runs on the REMOTE host, not locally. const sinceFlag = since ? `--since ${since}` : ""; const cmd = `cid=$(docker ps -a --filter name=${uuid} --format '{{.Names}}' | head -1); ` + `if [ -z "$cid" ]; then echo "NO_CONTAINER for ${uuid}"; exit 2; fi; ` + `echo "# container=$cid"; ` + `docker logs --timestamps ${sinceFlag} --tail ${tail} "$cid" 2>&1`; const { code, out, errOut } = await runRemote(cmd); if (out) process.stdout.write(out); if (errOut) process.stderr.write(errOut); process.exit(code ?? 0);