From 33a0e8a584b958b3c9e4a7a6d0ebdbc75242cba2 Mon Sep 17 00:00:00 2001 From: Marijn Doeve Date: Fri, 10 Jul 2026 18:14:39 +0200 Subject: [PATCH] Avoid dev port clashes and isolate docker compose per git worktree (#203) * Avoid dev port clashes and isolate docker compose per git worktree Default dev ports (80/443/5432) clash with other projects' compose stacks. Remap them to 8080/8443/5433+ by default, and add `just init` (auto-run by `just up`) to generate a per-worktree `.env.local` with a unique COMPOSE_PROJECT_NAME, image tag, and free host ports, so multiple worktrees can run `just up` concurrently without sharing containers, volumes, images, or ports. * Pin CI to port 80 for the HTTP reachability check; use 5430 as default Postgres dev port CI runs in an isolated single-purpose runner with no port-clash concern, so pin the HTTP reachability check back to port 80 explicitly rather than changing the dev default. Also move the default dev Postgres port range from 5433 to 5430, since 5433 clashes with other commonly used local projects. * Fix Justfile init: propagate free_port failures and use portable hash free_port exhaustion was swallowed inside a command substitution used as an echo argument, silently writing empty ports to .env.local. shasum is also not guaranteed on stripped-down Linux hosts. --- .github/workflows/ci.yml | 2 ++ CLAUDE.md | 18 ++++++++++++ Justfile | 63 +++++++++++++++++++++++++++++++++++++++- README.md | 9 ++++-- compose.override.yaml | 12 ++++---- 5 files changed, 95 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8644fbc..18d7428 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,6 +70,8 @@ jobs: set: | *.cache-from=type=gha,scope=${{github.ref}}-devbuild - name: Start services + env: + HTTP_PORT: "80" run: docker compose up php database --wait --no-build - name: Warm up dev cache run: docker compose exec -T php bin/console cache:warmup --env=dev diff --git a/CLAUDE.md b/CLAUDE.md index 01a09a2..ced2ff6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,6 +40,24 @@ just shell # Interactive shell inside the PHP container just shell-run # Shell in a fresh one-off container ``` +### Working in git worktrees + +`just up` auto-runs `just init` first, which generates a gitignored `.env.local` per checkout with a unique +`COMPOSE_PROJECT_NAME`, `IMAGES_PREFIX`, and free `HTTP_PORT`/`HTTPS_PORT`/`POSTGRES_PORT`/`MAILPIT_PORT`/ +`SPOTLIGHT_PORT`. This means every worktree gets its own containers, network, volumes, and image tag — running +`just up` in two worktrees at the same time does **not** make them share a database, image, or port, even if the +worktree directories have the same basename. + +- Run `just ports` to see the ports assigned to the *current* checkout — the app for that worktree is at + `https://localhost:`, not a fixed port. Never assume port 8080/8443/5432/etc. when working inside a + worktree; always check `.env.local` or `just ports` first. +- `.env.local` is generated once and reused; it's safe to run `just init`/`just up` repeatedly. Delete `.env.local` + and re-run `just init` to force new ports (e.g. if the assigned ones are now taken by something else). +- Each worktree's Postgres data, uploaded files, and Caddy state live in per-worktree Docker volumes — nothing is + shared with the main checkout or other worktrees. Migrations/fixtures must be (re-)run per worktree. +- `just down`/`just clean` in one worktree only ever affects that worktree's own containers/volumes — safe to run + without impacting other worktrees. + ### Database ```bash diff --git a/Justfile b/Justfile index 3ea6bc6..dfd5d6e 100644 --- a/Justfile +++ b/Justfile @@ -1,9 +1,70 @@ -up *args: +# Load per-worktree overrides (project name, image tag, ports) generated by `just init` +set dotenv-load := true +set dotenv-filename := ".env.local" + +# Generate a per-worktree COMPOSE_PROJECT_NAME, IMAGES_PREFIX and free host ports in .env.local, +# so multiple worktrees/checkouts of this repo can run `just up` at the same time without their +# containers, volumes, images or ports colliding. Safe to re-run; no-ops if already configured. +init: + #!/usr/bin/env bash + set -euo pipefail + if [ -f .env.local ] && grep -q '^COMPOSE_PROJECT_NAME=' .env.local; then + echo ".env.local already configured for this worktree, skipping (delete it to regenerate)." + exit 0 + fi + free_port() { + local port="$1" max="$2" + while [ "$port" -le "$max" ]; do + if ! (exec 3<>/dev/tcp/127.0.0.1/"$port") 2>/dev/null; then + echo "$port" + return + fi + exec 3>&- 2>/dev/null || true + port=$((port + 1)) + done + echo "no free port found between $1 and $2" >&2 + exit 1 + } + if command -v sha1sum >/dev/null 2>&1; then + hash_cmd=(sha1sum) + elif command -v shasum >/dev/null 2>&1; then + hash_cmd=(shasum) + else + hash_cmd=(sha256sum) + fi + hash=$(pwd | "${hash_cmd[@]}" | cut -c1-8) + project="tvdt-${hash}" + http_port=$(free_port 8080 8179) + https_port=$(free_port 8443 8542) + postgres_port=$(free_port 5430 5529) + mailpit_port=$(free_port 8025 8124) + spotlight_port=$(free_port 8969 9068) + { + echo "COMPOSE_PROJECT_NAME=${project}" + echo "IMAGES_PREFIX=${project}-" + echo "HTTP_PORT=${http_port}" + echo "HTTPS_PORT=${https_port}" + echo "POSTGRES_PORT=${postgres_port}" + echo "MAILPIT_PORT=${mailpit_port}" + echo "SPOTLIGHT_PORT=${spotlight_port}" + } >> .env.local + echo "Generated .env.local for this worktree:" + cat .env.local + +up *args: init + #!/usr/bin/env bash + set -a + [ -f .env.local ] && source .env.local + set +a docker compose up -d {{ args }} down *args: docker compose down --remove-orphans {{ args }} +# Show the host ports assigned to this worktree (see `just init`) +ports: + @cat .env.local 2>/dev/null || echo "No .env.local yet, run 'just up' or 'just init' first." + stop: docker compose stop diff --git a/README.md b/README.md index c6e28ec..86adf97 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,13 @@ just migrate # Run pending database migrations just fixtures # Load dev fixtures (truncates first) ``` -The app is available at **https://localhost** (self-signed cert — run -`just trust-cert` on macOS to trust it). +`just up` first runs `just init`, which generates a `.env.local` (gitignored) +with a unique `COMPOSE_PROJECT_NAME`, image tag and free host ports for this +checkout, so multiple worktrees/clones can run at the same time without their +containers, volumes, images or ports colliding. Run `just ports` to see the +ports assigned to the current checkout — the app is served at +`https://localhost:` (self-signed cert — run `just trust-cert` on +macOS to trust it). ### Useful commands diff --git a/compose.override.yaml b/compose.override.yaml index 7c718e0..b0f1cda 100644 --- a/compose.override.yaml +++ b/compose.override.yaml @@ -24,15 +24,15 @@ services: ports: # HTTP - target: 80 - published: ${HTTP_PORT:-80} + published: ${HTTP_PORT:-8080} protocol: tcp # HTTPS - target: 443 - published: ${HTTPS_PORT:-443} + published: ${HTTPS_PORT:-8443} protocol: tcp # HTTP/3 - target: 443 - published: ${HTTP3_PORT:-443} + published: ${HTTPS_PORT:-8443} protocol: udp sass: image: ${IMAGES_PREFIX:-}app-php @@ -56,7 +56,7 @@ services: ###> doctrine/doctrine-bundle ### database: ports: - - "5432:5432" + - "${POSTGRES_PORT:-5430}:5432" ###< doctrine/doctrine-bundle ### ###> symfony/mailer ### @@ -64,7 +64,7 @@ services: image: axllent/mailpit ports: - "1025" - - "8025:8025" + - "${MAILPIT_PORT:-8025}:8025" environment: MP_SMTP_AUTH_ACCEPT_ANY: 1 MP_SMTP_AUTH_ALLOW_INSECURE: 1 @@ -73,7 +73,7 @@ services: spotlight: image: ghcr.io/getsentry/spotlight:latest ports: - - "8969:8969" + - "${SPOTLIGHT_PORT:-8969}:8969" volumes: sass: