( options: StartDevServerOptions, reuseKey: string )
| 466 | } |
| 467 | |
| 468 | async function startContainer( |
| 469 | options: StartDevServerOptions, |
| 470 | reuseKey: string |
| 471 | ): Promise<StartDevServerResult> { |
| 472 | return withSpan( |
| 473 | options.span, |
| 474 | 'container.dev.start', |
| 475 | { 'service.name': options.service?.name }, |
| 476 | async span => { |
| 477 | const { config, meta, onStdout, onStderr } = options; |
| 478 | const out: DevOutput = { onStdout, onStderr }; |
| 479 | |
| 480 | // Fail fast with a clear message if Docker isn't running, rather than |
| 481 | // letting `docker build`/`docker run` fail later with a bare exit code. |
| 482 | await assertDockerAvailable(out); |
| 483 | |
| 484 | const image = await withSpan(span, 'container.dev.resolve_image', {}, s => |
| 485 | resolveDevImage(options, out, s) |
| 486 | ); |
| 487 | |
| 488 | const containerPort = await resolveContainerPort(image, out); |
| 489 | const containerName = uniqueContainerName( |
| 490 | options.service?.name ?? 'service' |
| 491 | ); |
| 492 | |
| 493 | // Env precedence: CLI process env, then the orchestrator's per-service |
| 494 | // env (service URLs, resolved .env values), then a `PORT` the app honors. |
| 495 | // Host/shell-only vars (TMPDIR, HOME, PATH, …) are filtered out: they |
| 496 | // describe the developer's machine, not the Linux container, and leaking |
| 497 | // them breaks apps that rely on container-native values (see |
| 498 | // `isHostOnlyEnvVar`). Passed via a temp `--env-file` to keep secrets off |
| 499 | // the command line and avoid arg-length limits. |
| 500 | const mergedEnv: Record<string, string> = {}; |
| 501 | for (const [key, value] of Object.entries(process.env)) { |
| 502 | if (typeof value === 'string' && !isHostOnlyEnvVar(key)) { |
| 503 | mergedEnv[key] = value; |
| 504 | } |
| 505 | } |
| 506 | const metaEnv = (meta?.env ?? {}) as Record<string, string | undefined>; |
| 507 | for (const [key, value] of Object.entries(metaEnv)) { |
| 508 | if (typeof value === 'string' && !isHostOnlyEnvVar(key)) { |
| 509 | mergedEnv[key] = value; |
| 510 | } |
| 511 | } |
| 512 | mergedEnv.PORT = String(containerPort); |
| 513 | const envFilePath = writeEnvFile(mergedEnv); |
| 514 | |
| 515 | const command = normalizeCommand( |
| 516 | (config as { command?: unknown }).command |
| 517 | ); |
| 518 | |
| 519 | // Honor the host port the orchestrator pre-allocated for this service |
| 520 | // (passed via `meta.port`). Service bindings are built against this port |
| 521 | // (`http://127.0.0.1:${preAllocatedPort}/`), so the container must listen |
| 522 | // on it for cross-service requests to reach it. Fall back to `0` (an |
| 523 | // ephemeral port chosen by Docker) when no port was provided. |
| 524 | const requestedHostPort = typeof meta?.port === 'number' ? meta.port : 0; |
| 525 |
no test coverage detected