Validate persisted ports against live host on every deploy

Previously the script only ran port resolution when no compose
containers were already running, on the assumption that running
containers meant the persisted port was still ours. On the shared
optical-dev server that's wrong: another app can grab a port
between our deploys, leaving us with a stale .env value that fails
to bind.

Now find_free_port treats a port as "available for us" if either
nothing is listening, or one of our own compose containers is
publishing it (so re-deploys don't shuffle). Other-app listeners
trigger a re-pick and a warning.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
DJP 2026-04-29 11:22:06 -04:00
parent 8e969fe015
commit 88e4df59be

View file

@ -84,17 +84,35 @@ port_in_use() {
[[ -n "$pid" ]]
}
# Echo the compose project name of the docker container publishing $port,
# or empty string if no docker container holds it (might be a host process
# or a non-published container).
port_owner_project() {
local port=$1
docker ps --format '{{.Label "com.docker.compose.project"}}|{{.Ports}}' 2>/dev/null \
| awk -F'|' -v p=":${port}->" '$2 ~ p {print $1; exit}'
}
# True (0) if the port is free for us to bind: either nothing's listening,
# or one of our own compose containers is publishing it (recreating the
# stack will hand it back to us). Other-process listeners → false.
port_available_for_us() {
local port=$1
if ! port_in_use "$port"; then return 0; fi
[[ "$(port_owner_project "$port")" == "$COMPOSE_PROJECT" ]]
}
find_free_port() {
local preferred=$1
local start=$2
local end=$3
if ! port_in_use "$preferred"; then
if port_available_for_us "$preferred"; then
printf '%s' "$preferred"
return 0
fi
local p
for ((p=start; p<=end; p++)); do
if ! port_in_use "$p"; then
if port_available_for_us "$p"; then
printf '%s' "$p"
return 0
fi
@ -162,26 +180,25 @@ PREV_APP_PORT="$APP_PORT"
log "Resolving host ports (preferred: app=$APP_PORT db=$DB_PORT)…"
RUNNING=$(docker compose ps -q 2>/dev/null | wc -l | tr -d ' ')
if [[ "$RUNNING" -gt 0 ]]; then
ok "Project '$COMPOSE_PROJECT' has $RUNNING container(s) running — keeping current port assignment."
else
NEW_APP_PORT=$(find_free_port "$APP_PORT" 3102 3199) || NEW_APP_PORT=""
NEW_DB_PORT=$(find_free_port "$DB_PORT" 5435 5499) || NEW_DB_PORT=""
if [[ -z "$NEW_APP_PORT" || -z "$NEW_DB_PORT" ]]; then
err "Could not find free ports."
err " app desired=$APP_PORT scanned=3102-3199"
err " db desired=$DB_PORT scanned=5435-5499"
exit 1
fi
[[ "$NEW_APP_PORT" != "$APP_PORT" ]] && warn "app port $APP_PORT busy → using $NEW_APP_PORT"
[[ "$NEW_DB_PORT" != "$DB_PORT" ]] && warn "db port $DB_PORT busy → using $NEW_DB_PORT"
APP_PORT=$NEW_APP_PORT
DB_PORT=$NEW_DB_PORT
set_env_var ADEO_PORT "$APP_PORT"
set_env_var ADEO_DB_PORT "$DB_PORT"
ok "Ports: app=$APP_PORT db=$DB_PORT (persisted to .env)"
# Always validate against live host state. `find_free_port` returns the
# preferred port if it's free OR already held by one of our own compose
# containers (so re-deploys don't shuffle ports). If another app on the
# shared host has grabbed the port, we re-pick.
NEW_APP_PORT=$(find_free_port "$APP_PORT" 3102 3199) || NEW_APP_PORT=""
NEW_DB_PORT=$(find_free_port "$DB_PORT" 5435 5499) || NEW_DB_PORT=""
if [[ -z "$NEW_APP_PORT" || -z "$NEW_DB_PORT" ]]; then
err "Could not find free ports."
err " app desired=$APP_PORT scanned=3102-3199"
err " db desired=$DB_PORT scanned=5435-5499"
exit 1
fi
[[ "$NEW_APP_PORT" != "$APP_PORT" ]] && warn "app port $APP_PORT taken by another app → using $NEW_APP_PORT"
[[ "$NEW_DB_PORT" != "$DB_PORT" ]] && warn "db port $DB_PORT taken by another app → using $NEW_DB_PORT"
APP_PORT=$NEW_APP_PORT
DB_PORT=$NEW_DB_PORT
set_env_var ADEO_PORT "$APP_PORT"
set_env_var ADEO_DB_PORT "$DB_PORT"
ok "Ports: app=$APP_PORT db=$DB_PORT (persisted to .env)"
# ---------- 4. Render apache-adeo.conf from template ----------