Running Docker-based tests concurrently creates collision chaos: containers share names, ports conflict, volumes leak state, and Docker hits hard resource limits. Tests pass in isolation, fail in parallel. CI becomes flaky. Developers lose trust.
Container isolation provides four mechanisms to ensure tests never interfere:
- Unique container names:
{test-name}-{pid}-{random}-{service}guarantees no name collisions - Dynamic port allocation: Check available ports from range (10000-65535), assign only confirmed-free ports
- Transient volumes: Named volumes disappear with
docker compose down -v— no state leakage - Per-test compose files:
docker-compose-{test-name}-{pid}-{random}-{timestamp}.ymlensures file-level isolation
Beyond collision prevention, isolation solves Docker resource exhaustion. Docker has hard limits: ~31 networks per bridge driver, finite containers and volumes. Without cleanup hygiene, concurrent tests exhaust these limits and crash the Docker daemon. Aggressive cleanup (down -v, volume removal, network pruning) combined with isolation prevents resource exhaustion.
Port allocation utility:
async function allocatePorts(count: number): Promise<number[]> {
const ports: number[] = [];
const portRange = { min: 10000, max: 65535 };
for (let i = 0; i < count; i++) {
let port: number;
let available = false;
let attempts = 0;
while (!available && attempts < 100) {
port = randomInt(portRange.min, portRange.max);
available = await isPortAvailable(port);
attempts++;
}
if (!available) {
throw new Error(`Could not allocate ${count} ports`);
}
ports.push(port);
}
return ports;
}Compose file generation:
function generateComposeFile(testName: string, ports: Record<string, number>): string {
const filename = `docker-compose-${testName}-${process.pid}-${randomBytes(4).toString('hex')}-${Date.now()}.yml`;
const content = `
version: '3.8'
services:
app:
container_name: ${testName}-${process.pid}-${randomBytes(4).toString('hex')}-app
ports:
- "${ports.app}:3000"
environment:
- DB_HOST=postgres
- DB_PORT=${ports.postgres}
# ...
postgres:
container_name: ${testName}-${process.pid}-${randomBytes(4).toString('hex')}-postgres
ports:
- "${ports.postgres}:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
postgres-data:
`;
await writeFile(filename, content);
return filename;
}Cleanup is critical:
async function cleanup(composeFile: string): Promise<void> {
await exec('docker', ['compose', '-f', composeFile, 'down', '-v', '--remove-orphans']);
await unlink(composeFile);
}Don't use hardcoded ports like 3000, 5432, 6379. These collide on the developer's machine and in CI. Always allocate dynamically.
Don't use shared volumes for "performance." State leakage causes flaky tests. Transient volumes are slightly slower but reliable.
Don't rely on Docker's auto-cleanup. Networks and orphaned containers accumulate. Explicit removal in every test's teardown is required.
- Pattern 1.1 — Stack Tests: Isolation enables stack tests to run concurrently
- L3 — Test Orchestration: The orchestration layer manages isolation at scale
Back to L1 Overview | Previous: Pattern 1.3 | Next: Pattern 1.5
