From 4e6baf0313bbe3efc63f55eb7c7e940e3f81c10e Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Tue, 24 Mar 2026 08:55:58 -0700 Subject: [PATCH] fix: replace host networking with exposed ports for macOS compatibility Fixes #66. The testcontainers Tempo instance used withNetworkMode('host') which only works on Linux. On macOS, Docker Desktop runs in a VM so host networking doesn't expose ports to the Mac host. Changes: - testcontainers/Instance.ts: Replace withNetworkMode('host') with withExposedPorts(containerPort), return dynamic {host, port} from started.getHost()/getMappedPort() after container start - Instance.ts: Widen internal start() return type to allow {host?, port?}, convert host/port to let+getters so they update dynamically after start resolves - Instance.test.ts: Add regression tests for dynamic host/port propagation and void-return backward compatibility --- src/Instance.test.ts | 39 ++++++++++++++++++++++++++++++++++ src/Instance.ts | 18 ++++++++++++---- src/testcontainers/Instance.ts | 17 +++++++++------ 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/src/Instance.test.ts b/src/Instance.test.ts index fe68de5..5156202 100644 --- a/src/Instance.test.ts +++ b/src/Instance.test.ts @@ -305,6 +305,45 @@ test('behavior: messages', async () => { `) }) +test('behavior: dynamic host/port via setEndpoint', async () => { + const foo = Instance.define(() => { + return { + name: 'foo', + host: 'localhost', + port: 3000, + async start(_, { setEndpoint }) { + setEndpoint?.({ host: '192.168.1.100', port: 9999 }) + }, + async stop() {}, + } + }) + + const instance = foo() + expect(instance.host).toEqual('localhost') + expect(instance.port).toEqual(3000) + + await instance.start() + expect(instance.host).toEqual('192.168.1.100') + expect(instance.port).toEqual(9999) +}) + +test('behavior: start() returning void keeps original host/port', async () => { + const foo = Instance.define(() => { + return { + name: 'foo', + host: 'localhost', + port: 3000, + async start() {}, + async stop() {}, + } + }) + + const instance = foo() + await instance.start() + expect(instance.host).toEqual('localhost') + expect(instance.port).toEqual(3000) +}) + test('options: timeout', async () => { const foo = Instance.define(() => { return { diff --git a/src/Instance.ts b/src/Instance.ts index e473e57..b110976 100644 --- a/src/Instance.ts +++ b/src/Instance.ts @@ -121,11 +121,12 @@ export function define< const options = options_ || parametersOrOptions || {} const instance = fn(parameters) - const { _internal, host, name, port, start, stop } = { + const { _internal, name, start, stop } = { ...instance, ...createParameters, - port: createParameters.port ?? instance.port, } + let host = instance.host + let port = createParameters?.port ?? instance.port const { messageBuffer = 20, timeout } = options let restartResolver = Promise.withResolvers() @@ -159,9 +160,13 @@ export function define< return messages }, }, - host, + get host() { + return host + }, name, - port, + get port() { + return port + }, get status() { if (restarting) return 'restarting' return status @@ -193,6 +198,10 @@ export function define< }, { emitter, + setEndpoint(endpoint) { + if (endpoint.host) host = endpoint.host + if (endpoint.port) port = endpoint.port + }, status: this.status, }, ) @@ -302,6 +311,7 @@ export declare namespace define { export type InstanceStartOptions_internal = { emitter: EventEmitter + setEndpoint?(endpoint: { host?: string; port?: number }): void status: Instance['status'] } diff --git a/src/testcontainers/Instance.ts b/src/testcontainers/Instance.ts index 78efd6f..08a06cd 100644 --- a/src/testcontainers/Instance.ts +++ b/src/testcontainers/Instance.ts @@ -47,20 +47,21 @@ export const tempo = Instance.define((parameters?: tempo.Parameters) => { host: args.host ?? 'localhost', name, port: args.port ?? 8545, - async start({ port = args.port }, { emitter }) { + async start({ port = args.port }, { emitter, setEndpoint }) { const promise = Promise.withResolvers() + const containerPort = port ?? 8545 + const c = new GenericContainer(image) .withPullPolicy(PullPolicy.alwaysPull()) .withPlatform('linux/x86_64') - .withNetworkMode('host') + .withExposedPorts(containerPort) .withExtraHosts([ { host: 'host.docker.internal', ipAddress: 'host-gateway' }, - { host: 'localhost', ipAddress: 'host-gateway' }, ]) .withName(containerName) .withEnvironment({ RUST_LOG }) - .withCommand(command({ ...args, port })) + .withCommand(command({ ...args, port: containerPort })) .withWaitStrategy( Wait.forLogMessage(/Received (block|new payload) from consensus engine/), ) @@ -83,8 +84,12 @@ export const tempo = Instance.define((parameters?: tempo.Parameters) => { .withStartupTimeout(10_000) c.start() - .then((c) => { - container = c + .then((started) => { + container = started + setEndpoint?.({ + host: started.getHost(), + port: started.getMappedPort(containerPort), + }) promise.resolve() }) .catch(promise.reject)