TryGhost/Ghost · warning · Error
Egress monitor sidecar did not start in time
Error message
Egress monitor sidecar did not start in time
What it means
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.
Source
Thrown at e2e/helpers/environment/service-managers/egress-monitor.ts:125
}
/**
* Wait until CoreDNS has logged its startup banner (it prints `CoreDNS-<ver>`
* once it is serving). Throws on timeout so start() stays fail-open and the
* caller falls back to Docker's default resolver.
*/
private async waitForListening(container: Container, timeoutMs = 10000): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const buffer = await container.logs({stdout: true, stderr: true, follow: false, timestamps: false});
if (buffer.toString('utf8').includes('CoreDNS-')) {
return;
}
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
}
throw new Error('Egress monitor sidecar did not start in time');
}
private async getOrCreate(): Promise<Container> {
const existing = this.docker.getContainer(this.containerName);
try {
const info = await existing.inspect();
if (info.State.Running) {
return existing;
}
await existing.start();
return existing;
} catch (error) {
const statusCode = (error as {statusCode?: number})?.statusCode;
const message = error instanceof Error ? error.message : String(error);
if (statusCode !== 404 && !/No such container/i.test(message)) {
throw error;
}
}View on GitHub (pinned to 47d8b0e2ad)
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.
Example fix
// before: fixed 10s budget shared between image pull and banner wait await this.waitForListening(this.container); // after: separate the pull from the wait, and give a generous banner window on CI await this.ensureImage(); // image cached → pull is instant this.container = await this.getOrCreate(); await this.waitForListening(this.container, process.env.CI ? 30000 : 10000);
Defensive patterns
Strategy: retry
Validate before calling
// Pre-pull the image so the startup budget isn't spent on the pull
async function ensureImageCached(docker, image) {
try {
await docker.getImage(image).inspect();
} catch {
const stream = await docker.pull(image);
await new Promise((resolve, reject) =>
docker.modem.followProgress(stream, err => err ? reject(err) : resolve()));
}
}
await ensureImageCached(docker, EGRESS_DNS_IMAGE); Try / catch
// start() already catches this and fails open; if you call waitForListening directly, wrap it:
async function startWithRetry(monitor) {
try {
await monitor.start();
} catch (err) {
if (/did not start in time/i.test(err.message)) {
console.warn('Egress monitor unavailable — continuing without DNS monitoring');
return;
}
throw err;
}
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Cannot create without a persistence adapter. Use buildMany()
- Failed to create ${entityType}: ${response.status()} ${error
- ${entityType} with id ${id} not found
- Failed to update ${entityType}: ${response.status()}
- Cannot insert without an id field
AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13).
Data as JSON: /api/errors/298c7c3d6b087820.
Report an issue: GitHub.