diff --git a/deploy/targets/local-docker.mjs b/deploy/targets/local-docker.mjs index 8bcd8c4..9d3a456 100644 --- a/deploy/targets/local-docker.mjs +++ b/deploy/targets/local-docker.mjs @@ -4,9 +4,24 @@ import fs from 'node:fs/promises' import path from 'node:path' +import net from 'node:net' import { spawn } from 'node:child_process' import crypto from 'node:crypto' +// Ask the OS for a free ephemeral port on the loopback interface. Far safer +// than a random pick under concurrent deploys, where two cells could otherwise +// choose the same port and the second `docker run -p` would fail. +function findFreePort() { + return new Promise((resolve, reject) => { + const srv = net.createServer() + srv.on('error', reject) + srv.listen(0, '127.0.0.1', () => { + const { port } = srv.address() + srv.close(() => resolve(port)) + }) + }) +} + function run(cmd, args, opts = {}) { return new Promise((resolve, reject) => { const p = spawn(cmd, args, { stdio: opts.quiet ? 'pipe' : 'inherit', ...opts }) @@ -84,8 +99,8 @@ export async function deployLocalDocker({ manifest, manifestPath, repoRoot }) { // Tear down any old container with the same name (idempotency for re-deploys) await run('docker', ['rm', '-f', containerName], { quiet: true }).catch(() => {}) - // Pick a free local port - const port = 8000 + Math.floor(Math.random() * 1000) + // Pick a free local port (OS-assigned, avoids collisions across concurrent deploys) + const port = await findFreePort() // Run detached console.log(` [docker run] ${containerName} on :${port}`) diff --git a/polyrange.mjs b/polyrange.mjs index 2285a94..71a7a1f 100644 --- a/polyrange.mjs +++ b/polyrange.mjs @@ -273,8 +273,10 @@ async function cmdDestroy() { async function cmdOne() { const cls = flags.class const tier = flags.tier ?? '0' + const target = flags.target ?? 'fly' // fly | local-docker if (!cls) throw new Error('--class=X is required') - const args = ['generator/deploy.mjs', `--class=${cls}`, `--tier=${tier}`, '--target=fly'] + const args = ['generator/deploy.mjs', `--class=${cls}`, `--tier=${tier}`, `--target=${target}`] + if (flags.region) args.push(`--region=${flags.region}`) if (flags.ephemeral) args.push('--ephemeral') const child = spawn('node', args, { stdio: 'inherit', cwd: REPO_ROOT, env: process.env }) await new Promise((resolve) => child.on('close', resolve))