{"record":{"id":"298c7c3d6b087820","repo":"TryGhost/Ghost","slug":"egress-monitor-sidecar-did-not-start-in-time","errorCode":null,"errorMessage":"Egress monitor sidecar did not start in time","messagePattern":"Egress monitor sidecar did not start in time","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"e2e/helpers/environment/service-managers/egress-monitor.ts","lineNumber":125,"sourceCode":"    }\n\n    /**\n     * Wait until CoreDNS has logged its startup banner (it prints `CoreDNS-<ver>`\n     * once it is serving). Throws on timeout so start() stays fail-open and the\n     * caller falls back to Docker's default resolver.\n     */\n    private async waitForListening(container: Container, timeoutMs = 10000): Promise<void> {\n        const deadline = Date.now() + timeoutMs;\n        while (Date.now() < deadline) {\n            const buffer = await container.logs({stdout: true, stderr: true, follow: false, timestamps: false});\n            if (buffer.toString('utf8').includes('CoreDNS-')) {\n                return;\n            }\n            await new Promise((resolve) => {\n                setTimeout(resolve, 100);\n            });\n        }\n        throw new Error('Egress monitor sidecar did not start in time');\n    }\n\n    private async getOrCreate(): Promise<Container> {\n        const existing = this.docker.getContainer(this.containerName);\n        try {\n            const info = await existing.inspect();\n            if (info.State.Running) {\n                return existing;\n            }\n            await existing.start();\n            return existing;\n        } catch (error) {\n            const statusCode = (error as {statusCode?: number})?.statusCode;\n            const message = error instanceof Error ? error.message : String(error);\n            if (statusCode !== 404 && !/No such container/i.test(message)) {\n                throw error;\n            }\n        }","sourceCodeStart":107,"sourceCodeEnd":143,"githubUrl":"https://github.com/TryGhost/Ghost/blob/47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe/e2e/helpers/environment/service-managers/egress-monitor.ts#L107-L143","documentation":"Thrown by EgressMonitor.waitForListening() when the CoreDNS sidecar container didn't log its 'CoreDNS-' startup banner within the 10s timeout. The monitor polls container logs every 100ms looking for the banner. Crucially this is a fail-open component: start() catches the throw, logs, and leaves the monitor inactive so the e2e suite falls back to Docker's default DNS — the error is internal and normally swallowed, not test-fatal.","triggerScenarios":"The CoreDNS container started but was slow to bind (>10s — slow CI, loaded Docker daemon, slow image layer init); the container exited immediately (bad Corefile, port 53 conflict, permission issue despite User:'0:0'); image pull was slow and ate into the window; the container is running but logging to a stream the logs() call isn't capturing.","commonSituations":"CI runner under heavy load so the sidecar takes >10s to print its banner; EGRESS_DNS_IMAGE not cached locally so the pull (inside ensureImage) consumed the budget before the container even started; another process on the host bound port 53; the Corefile at EGRESS_COREFILE_PATH is malformed so CoreDNS logs an error instead of the banner; Docker daemon slow after a large image load.","solutions":["Pre-pull the EGRESS_DNS_IMAGE on CI runners so ensureImage() is a no-op and the full 10s budget goes to startup.","Raise the waitForListening timeout (pass a larger timeoutMs) on known-slow CI — but prefer fixing the root cause over a longer wait.","Inspect the sidecar logs manually: 'docker logs ghost-e2e-egress-worker-<n>' — a malformed Corefile or port-53 conflict shows up here.","Confirm EGRESS_COREFILE_PATH points at a valid Corefile that CoreDNS accepts.","Free port 53 on the host (or ensure the container's network namespace isolates it).","Since the monitor is fail-open, confirm the suite still passed — this error only degrades DNS egress observability, it does not break tests."],"exampleFix":"// before: fixed 10s budget shared between image pull and banner wait\nawait this.waitForListening(this.container);\n\n// after: separate the pull from the wait, and give a generous banner window on CI\nawait this.ensureImage(); // image cached → pull is instant\nthis.container = await this.getOrCreate();\nawait this.waitForListening(this.container, process.env.CI ? 30000 : 10000);","handlingStrategy":"retry","validationCode":"// Pre-pull the image so the startup budget isn't spent on the pull\nasync function ensureImageCached(docker, image) {\n    try {\n        await docker.getImage(image).inspect();\n    } catch {\n        const stream = await docker.pull(image);\n        await new Promise((resolve, reject) =>\n            docker.modem.followProgress(stream, err => err ? reject(err) : resolve()));\n    }\n}\nawait ensureImageCached(docker, EGRESS_DNS_IMAGE);","typeGuard":null,"tryCatchPattern":"// start() already catches this and fails open; if you call waitForListening directly, wrap it:\nasync function startWithRetry(monitor) {\n    try {\n        await monitor.start();\n    } catch (err) {\n        if (/did not start in time/i.test(err.message)) {\n            console.warn('Egress monitor unavailable — continuing without DNS monitoring');\n            return;\n        }\n        throw err;\n    }\n}","preventionTips":["Pre-pull the EGRESS_DNS_IMAGE on CI runners so the 10s budget is purely for banner wait.","Confirm the Corefile at EGRESS_COREFILE_PATH is valid before the run.","Free port 53 / avoid DNS conflicts on the host.","Remember the monitor is fail-open — its absence only loses egress observability, it does not fail tests."],"tags":["e2e","docker","coredns","dns","sidecar","timeout","test-infrastructure"],"backgroundTag":null,"analyzedSha":"47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe","analyzedAt":"2026-08-13T01:25:26.651Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}