Common Local Failure Points
Diagnose and fix the local environment failures that block onboarding: port conflicts, dependency cache poisoning, env drift, seed races, and arch mismatch.
A new contributor's first day stalls on the same five breakages, year after year: a port already in use, a poisoned dependency cache, an .env that drifted from .env.example, a seed script that races the database, and a base image built for the wrong CPU architecture. This guide gives each one a deterministic diagnosis, a copy-paste fix, and a drift check so engineers can self-serve instead of escalating. It is part of the broader work on onboarding architecture and friction mapping, and the most acute single-symptom cases — containers that exit immediately on startup and DNS resolution failures between local containers — get dedicated walkthroughs.
Prerequisites
- Docker Engine 24+ with the Compose v2 plugin (
docker compose version). jq,lsof, andpg_isreadyavailable on the host (brew install jq lsof postgresqlon macOS; the Postgres client package on Linux).- A repository with a
docker-compose.yml, a committed lockfile, and a.env.example.
Port Allocation & Service Binding Conflicts
Hardcoded ports and implicit host bindings cause silent collisions that stop a service before it logs anything useful. Before bringing the stack up, audit what is already listening.
- List current listeners so you can spot the conflict:
#!/usr/bin/env bash set -euo pipefail lsof -iTCP -sTCP:LISTEN -P -n - Bind explicitly to loopback and parameterize the port so two stacks never fight over
3000:# docker-compose.yml services: app: container_name: local-app-primary build: . ports: - "127.0.0.1:${APP_PORT:-3000}:3000" environment: APP_PORT: "${APP_PORT:-3000}" - Run the drift check and reset stale bindings if the runtime mapping diverges from intent:
#!/usr/bin/env bash set -euo pipefail RUNTIME_PORTS=$(docker ps --format '{{.Ports}}' --filter "name=local-app-primary") EXPECTED_PORTS="127.0.0.1:${APP_PORT:-3000}->3000/tcp" if [ "$RUNTIME_PORTS" != "$EXPECTED_PORTS" ]; then echo "Port drift detected; recreating containers." >&2 docker compose down --remove-orphans docker compose up -d fi
The full Compose-specific fix for bind: address already in use lives in fixing "port is already allocated" errors in Compose.
Dependency Resolution & Cache Poisoning
Unpinned transitive dependencies and a stale package cache produce non-deterministic builds: the same git clone yields a different node_modules on two laptops. Pin exact versions in lockfiles and validate integrity.
- Install strictly from the lockfile and record its hash inside the dev container:
// .devcontainer/devcontainer.json { "name": "Local Dev Environment", "postCreateCommand": "npm ci --prefer-offline && sha256sum package-lock.json", "mounts": [ "source=npm-cache,target=/home/node/.npm,type=volume" ] } - Diff the resolved tree against a known-good baseline and fail on divergence:
#!/usr/bin/env bash set -euo pipefail npm ls --all --json > local-tree.json if ! diff -q ci-baseline-tree.json local-tree.json; then echo "Dependency drift detected versus CI baseline." >&2 exit 1 fi
When a build wedges on a transitive conflict, trace it with detecting circular dependencies in local builds and the broader dependency tree visualization workflow.
Apple Silicon (ARM64): native modules (
node-gyp,grpc) often fail to compile without architecture flags; setnpm_config_arch=arm64or--build-from-sourcein.npmrc. macOS (Docker Desktop): the:cached/:delegatedmount consistency flags can mask a poisoned cache; mount lockfiles read-only and rebuild from a clean volume to confirm. WSL2: keepnode_moduleson the Linux filesystem; the/mnt/ctranslation layer corrupts symlinks. Setcore.autocrlf=false.
Environment Variable & Secret Drift
When local .env falls behind .env.example, services boot with missing keys and fail at the first call that needs them. Enforce completeness before execution.
- Add a pre-commit hook that rejects an incomplete
.env:# .pre-commit-config.yaml repos: - repo: local hooks: - id: check-env name: Validate .env against schema entry: bash -c 'jq -e ".required | all(. as $k | env | has($k))" .env.schema.json' language: system files: '\.env$' - Surface the drift explicitly during diagnosis:
#!/usr/bin/env bash set -euo pipefail if ! diff -y --suppress-common-lines .env.example .env; then echo "Environment variable drift detected; reconcile required keys." >&2 fi
For schema-driven generation and validation that prevents this class entirely, see catching missing env vars before container startup.
WSL2: CRLF line endings make
.envparsers fail silently; rundos2unix .envor set Gitcore.eol=lf. macOS (Docker Desktop):.envresolves relative to the Compose project root, butdocker runresolves it against the working directory — always pass--env-filewith an explicit path.
Database State & Seed Script Execution Failures
Non-idempotent init scripts and missing readiness gates cause race conditions where the app connects before the schema exists. Gate startup on a health probe and make seeds deterministic.
- Block the app until the database is healthy:
# docker-compose.yml services: db: image: postgres:16-alpine environment: POSTGRES_PASSWORD: "${DB_PASS:-localdev}" POSTGRES_DB: app_db healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 2s timeout: 5s retries: 5 app: build: . depends_on: db: condition: service_healthy - Run an idempotent, readiness-gated seed:
#!/usr/bin/env bash set -euo pipefail export PGPASSWORD="${POSTGRES_PASSWORD:-localdev}" until pg_isready -h db -p 5432 -U postgres; do echo "Waiting for PostgreSQL readiness..." sleep 1 done psql -h db -U postgres -d app_db -f /docker-entrypoint-initdb.d/01-schema.sql psql -h db -U postgres -d app_db -f /docker-entrypoint-initdb.d/02-seed.sql
Startup-order races between healthy and dependent services are covered in depth in resolving service startup order and healthcheck races.
WSL2: bind-mounted DB volumes suffer severe I/O latency; use named volumes or a path under
~/, never/mnt/c. Apple Silicon (ARM64):postgres:16-alpineis multi-arch, but x86-only extensions (some PostGIS builds) requireplatform: linux/amd64and emulation.
Cross-Architecture Container Runtime Mismatch
Pulling an image built for the wrong architecture, or compiling a native extension for the wrong target, yields exec format error. Detect host architecture early and map it to a build platform.
- Make the build architecture-aware:
# Dockerfile ARG TARGETARCH=amd64 FROM node:20-alpine RUN apk add --no-cache python3 make g++ gcc - Diagnose a mismatch and rebuild for the host:
#!/usr/bin/env bash set -euo pipefail CONTAINER_ARCH=$(docker inspect --format='{{.Architecture}}' local-app-primary) HOST_ARCH=$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/') if [ "$CONTAINER_ARCH" != "$HOST_ARCH" ]; then echo "Architecture mismatch: container=$CONTAINER_ARCH host=$HOST_ARCH; rebuilding." >&2 docker compose build --build-arg "TARGETARCH=$HOST_ARCH" fi
Apple Silicon (ARM64): Docker Desktop defaults to
linux/arm64; emulatinglinux/amd64via Rosetta 2/QEMU is slow, so prefer native multi-arch base images. WSL2:uname -mreportsx86_64regardless of the underlying CPU; usedpkg --print-architecturefor accurate detection in scripts.
Rollback / Recovery
When a diagnostic fix leaves the stack in a worse state, return to a clean baseline in one move:
#!/usr/bin/env bash
set -euo pipefail
docker compose down -v --remove-orphans
docker network prune -f
git checkout -- docker-compose.yml .env.example
docker compose up -d --wait
This tears down containers and their volumes, clears dangling networks, restores tracked config, and brings a fresh stack up gated on health.