(
image: string,
env?: Record<string, string>,
)
| 79 | } |
| 80 | |
| 81 | private async startContainer( |
| 82 | image: string, |
| 83 | env?: Record<string, string>, |
| 84 | ): Promise<SandboxHandle> { |
| 85 | const exposed: Record<string, Record<string, never>> = {} |
| 86 | const bindings: Record<string, Array<{ HostPort: string }>> = {} |
| 87 | for (const port of this.config.publishPorts ?? []) { |
| 88 | exposed[`${port}/tcp`] = {} |
| 89 | bindings[`${port}/tcp`] = [{ HostPort: '' }] // let Docker pick a free host port |
| 90 | } |
| 91 | |
| 92 | const container = await this.docker.createContainer({ |
| 93 | Image: image, |
| 94 | Cmd: this.config.keepAliveCommand ?? ['sh', '-c', 'tail -f /dev/null'], |
| 95 | Tty: false, |
| 96 | WorkingDir: this.workdir, |
| 97 | Env: env ? Object.entries(env).map(([k, v]) => `${k}=${v}`) : undefined, |
| 98 | ExposedPorts: Object.keys(exposed).length ? exposed : undefined, |
| 99 | HostConfig: { |
| 100 | ...(Object.keys(bindings).length ? { PortBindings: bindings } : {}), |
| 101 | ...(this.config.hostGateway !== false |
| 102 | ? { ExtraHosts: ['host.docker.internal:host-gateway'] } |
| 103 | : {}), |
| 104 | }, |
| 105 | }) |
| 106 | |
| 107 | // If anything after createContainer fails (start, or the first exec to |
| 108 | // create the workspace dir), the container already exists and would leak as |
| 109 | // a stopped container — these accumulate and strain the daemon. Tear it down |
| 110 | // on any instantiation failure before propagating the error. |
| 111 | try { |
| 112 | await container.start() |
| 113 | const handle = new DockerHandle({ |
| 114 | docker: this.docker, |
| 115 | container, |
| 116 | workdir: this.workdir, |
| 117 | forkFactory: this.forkFactory, |
| 118 | removeOnDestroy: this.config.removeOnDestroy ?? true, |
| 119 | }) |
| 120 | // Ensure the workspace dir exists. |
| 121 | await handle.fs.mkdir(this.workdir) |
| 122 | return handle |
| 123 | } catch (error) { |
| 124 | try { |
| 125 | await container.remove({ force: true, v: true }) |
| 126 | } catch { |
| 127 | // best-effort cleanup — container may not have started / already gone |
| 128 | } |
| 129 | throw error |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | async create(input: SandboxCreateInput): Promise<SandboxHandle> { |
| 134 | await this.ensureImage(this.config.image) |
no test coverage detected