Repository: n8n-io/n8n
File: packages/testing/containers/services/ngrok.ts (TypeScript)

Code before:
					'Get a free token at https://dashboard.ngrok.com/get-started/your-authtoken',
			);
		}

		try {
			let builder = new GenericContainer(TEST_CONTAINER_IMAGES.ngrok)
				.withNetwork(network)
				.withNetworkAliases('ngrok')
				.withName(`${projectName}-ngrok`)
				.withExposedPorts(API_PORT)
				.withEnvironment({
					NGROK_AUTHTOKEN: authToken,
				})
				.withCommand(['http', `http://${tunnelTarget}`, '--log', 'stdout'])
				.withWaitStrategy(Wait.forLogMessage(/started tunnel/i))
				.withLabels({
					'com.docker.compose.project': projectName,
					'com.docker.compose.service': 'ngrok',
				})
				.withReuse()
				.withLogConsumer(consumer);

			// On Linux, host.docker.internal is not available without explicit mapping
			if (ctx?.external) {
				builder = builder.withExtraHosts([
					{ host: 'host.docker.internal', ipAddress: 'host-gateway' },
				]);
			}

			const container = await builder.start();

			const hostPort = container.getMappedPort(API_PORT);
			const host = container.getHost();

			// ngrok API returns tunnel info at /api/tunnels
			const response = await fetch(`http://${host}:${hostPort}/api/tunnels`);
			const data = (await response.json()) as {
				tunnels: Array<{ public_url: string; proto: string }>;
			};

			// Find the https tunnel
			const httpsTunnel = data.tunnels.find((t) => t.proto === 'https');
			const publicUrl = httpsTunnel?.public_url ?? data.tunnels[0]?.public_url;

			if (!publicUrl) {
				throw new Error('Failed to get ngrok public URL from API');
			}

			return {
				container,
				meta: { publicUrl, proxyHops },

Review comment (suggestion):
Could this also just point to `EXTERNAL_HOST` from `types.ts` ?

`{ host: EXTERNAL_HOST, ipAddress: 'host-gateway' },`

Code after:
					'Get a free token at https://dashboard.ngrok.com/get-started/your-authtoken',
			);
		}

		try {
			let builder = new GenericContainer(TEST_CONTAINER_IMAGES.ngrok)
				.withNetwork(network)
				.withNetworkAliases('ngrok')
				.withName(`${projectName}-ngrok`)
				.withExposedPorts(API_PORT)
				.withEnvironment({
					NGROK_AUTHTOKEN: authToken,
				})
				.withCommand(['http', `http://${tunnelTarget}`, '--log', 'stdout'])
				.withWaitStrategy(Wait.forLogMessage(/started tunnel/i))
				.withLabels({
					'com.docker.compose.project': projectName,
					'com.docker.compose.service': 'ngrok',
				})
				.withReuse()
				.withLogConsumer(consumer);

			// On Linux, host.docker.internal is not available without explicit mapping
			if (ctx?.external) {
				builder = builder.withExtraHosts([{ host: EXTERNAL_HOST, ipAddress: 'host-gateway' }]);
			}

			const container = await builder.start();

			const hostPort = container.getMappedPort(API_PORT);
			const host = container.getHost();

			// ngrok API returns tunnel info at /api/tunnels
			const response = await fetch(`http://${host}:${hostPort}/api/tunnels`);
			const data = (await response.json()) as {
				tunnels: Array<{ public_url: string; proto: string }>;
			};

			// Find the https tunnel
			const httpsTunnel = data.tunnels.find((t) => t.proto === 'https');
			const publicUrl = httpsTunnel?.public_url ?? data.tunnels[0]?.public_url;

			if (!publicUrl) {
				throw new Error('Failed to get ngrok public URL from API');
			}

			return {
				container,
				meta: { publicUrl, proxyHops },
			};
		} catch (error) {