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.
This commit is contained in:
2026-07-10 18:14:39 +02:00
committed by GitHub
parent 2fd15ba8fa
commit 33a0e8a584
5 changed files with 95 additions and 9 deletions
+2
View File
@@ -70,6 +70,8 @@ jobs:
set: | set: |
*.cache-from=type=gha,scope=${{github.ref}}-devbuild *.cache-from=type=gha,scope=${{github.ref}}-devbuild
- name: Start services - name: Start services
env:
HTTP_PORT: "80"
run: docker compose up php database --wait --no-build run: docker compose up php database --wait --no-build
- name: Warm up dev cache - name: Warm up dev cache
run: docker compose exec -T php bin/console cache:warmup --env=dev run: docker compose exec -T php bin/console cache:warmup --env=dev
+18
View File
@@ -40,6 +40,24 @@ just shell # Interactive shell inside the PHP container
just shell-run # Shell in a fresh one-off 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:<HTTPS_PORT>`, 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 ### Database
```bash ```bash
+62 -1
View File
@@ -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 }} docker compose up -d {{ args }}
down *args: down *args:
docker compose down --remove-orphans {{ 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: stop:
docker compose stop docker compose stop
+7 -2
View File
@@ -25,8 +25,13 @@ just migrate # Run pending database migrations
just fixtures # Load dev fixtures (truncates first) just fixtures # Load dev fixtures (truncates first)
``` ```
The app is available at **https://localhost** (self-signed cert — run `just up` first runs `just init`, which generates a `.env.local` (gitignored)
`just trust-cert` on macOS to trust it). 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:<HTTPS_PORT>` (self-signed cert — run `just trust-cert` on
macOS to trust it).
### Useful commands ### Useful commands
+6 -6
View File
@@ -24,15 +24,15 @@ services:
ports: ports:
# HTTP # HTTP
- target: 80 - target: 80
published: ${HTTP_PORT:-80} published: ${HTTP_PORT:-8080}
protocol: tcp protocol: tcp
# HTTPS # HTTPS
- target: 443 - target: 443
published: ${HTTPS_PORT:-443} published: ${HTTPS_PORT:-8443}
protocol: tcp protocol: tcp
# HTTP/3 # HTTP/3
- target: 443 - target: 443
published: ${HTTP3_PORT:-443} published: ${HTTPS_PORT:-8443}
protocol: udp protocol: udp
sass: sass:
image: ${IMAGES_PREFIX:-}app-php image: ${IMAGES_PREFIX:-}app-php
@@ -56,7 +56,7 @@ services:
###> doctrine/doctrine-bundle ### ###> doctrine/doctrine-bundle ###
database: database:
ports: ports:
- "5432:5432" - "${POSTGRES_PORT:-5430}:5432"
###< doctrine/doctrine-bundle ### ###< doctrine/doctrine-bundle ###
###> symfony/mailer ### ###> symfony/mailer ###
@@ -64,7 +64,7 @@ services:
image: axllent/mailpit image: axllent/mailpit
ports: ports:
- "1025" - "1025"
- "8025:8025" - "${MAILPIT_PORT:-8025}:8025"
environment: environment:
MP_SMTP_AUTH_ACCEPT_ANY: 1 MP_SMTP_AUTH_ACCEPT_ANY: 1
MP_SMTP_AUTH_ALLOW_INSECURE: 1 MP_SMTP_AUTH_ALLOW_INSECURE: 1
@@ -73,7 +73,7 @@ services:
spotlight: spotlight:
image: ghcr.io/getsentry/spotlight:latest image: ghcr.io/getsentry/spotlight:latest
ports: ports:
- "8969:8969" - "${SPOTLIGHT_PORT:-8969}:8969"
volumes: volumes:
sass: sass: