Files
multica/scripts/selfhost-wait.sh
Eve 3d64682a83 fix(selfhost): honour the full backend port alias chain in Compose
Addresses the second review round on #6168.

The contract this PR documents is

  BACKEND_PORT -> API_PORT -> SERVER_PORT -> PORT -> 8080

and Makefile, scripts/local-env.sh, scripts/install.sh, scripts/install.ps1 and
apps/web/config/runtime-urls.ts all implement it. docker-compose.selfhost.yml
implemented only BACKEND_PORT -> PORT -> 8080, so `API_PORT=9100` or
`SERVER_PORT=9200` in .env still published 8080.

That is not only a direct-Compose concern. scripts/install.sh:407 derives its
health-check port from the full chain and then runs this compose file, so an
alias it honours but Compose ignores puts the probe on 9100 while the stack
listens on 8080 — the original #6145 defect, reached through the installer.

  - docker-compose.selfhost.yml publishes the full nested chain, verified to
    keep the same precedence and the same 8080 default.
  - scripts/selfhost-wait.sh's fallback matches, so the degraded path agrees
    with what Compose would have published.
  - The Makefile captures the pristine origins of API_PORT and SERVER_PORT too,
    and the preflight reports them, so no alias can be silently overridden by
    the env file while the others are reported.

Tests pin the chain on the boundaries a recipe-level test cannot see, because
Makefile normalises all four aliases into PORT before any recipe runs: a matrix
over the direct Compose path asserts each alias moves the published port with
the right precedence, and every case additionally asserts that
scripts/install.sh's resolver returns the same value Compose published. That
invariant is the one that actually protects the installer.

Verified failing without the fix: with the two-level chain restored the suite
reports `expected 9200 / compose published 8080 / installer resolved 9200`, and
with the alias warnings narrowed it reports the missing API_PORT notice.

Co-authored-by: multica-agent <github@multica.ai>
2026-07-30 16:14:47 +08:00

97 lines
3.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# Wait for the self-hosted stack to answer /health, then print connection info.
# Shared by `make selfhost` and `make selfhost-build`.
#
# Why the host port is read back from Compose instead of re-derived here: the
# recipes used to probe `${PORT:-8080}` while Compose published `${BACKEND_PORT:-8080}`.
# Two different variables as the source of truth means any config where they
# disagree breaks — `.env` setting PORT with BACKEND_PORT unset probed the custom
# port while the stack sat on 8080, and `make selfhost PORT=8080` over a .env
# with BACKEND_PORT=9100 probed 8080 while the stack sat on 9100, hammering the
# wrong port for 60s and then reporting "Services are still starting" on a
# healthy install. `docker compose port` is the only authority on what Compose
# actually published, so ask it rather than re-deriving the answer.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
mode=${1:-official}
case "$mode" in
official)
compose_files=(-f docker-compose.selfhost.yml)
;;
build)
compose_files=(-f docker-compose.selfhost.yml -f docker-compose.selfhost.build.yml)
;;
*)
echo "usage: ${BASH_SOURCE[0]} [official|build]" >&2
exit 2
;;
esac
# The Makefile exports COMPOSE; keep the same default when run standalone.
read -r -a compose_cmd <<<"${COMPOSE:-docker compose}"
# Published host port for a service, straight from Compose. Falls back to the
# same default chain as docker-compose.selfhost.yml when the container is not up
# (e.g. `up -d` failed) so the message degrades instead of breaking.
compose_host_port() {
local service=$1 container_port=$2 fallback=$3 published
published=$("${compose_cmd[@]}" "${compose_files[@]}" port "$service" "$container_port" 2>/dev/null | tail -n 1) || published=""
published=${published##*:}
published=${published%$'\r'}
case "$published" in
'' | *[!0-9]*) printf '%s\n' "$fallback" ;;
*) printf '%s\n' "$published" ;;
esac
}
backend_port=$(compose_host_port backend 8080 "${BACKEND_PORT:-${API_PORT:-${SERVER_PORT:-${PORT:-8080}}}}")
frontend_port=$(compose_host_port frontend 3000 "${FRONTEND_PORT:-3000}")
backend_url="http://localhost:${backend_port}"
frontend_url="http://localhost:${frontend_port}"
health_ok() {
curl -sf "${backend_url}/health" >/dev/null 2>&1
}
echo "==> Waiting for backend to be ready..."
for _ in $(seq 1 30); do
if health_ok; then
break
fi
sleep 2
done
if ! health_ok; then
echo ""
echo "Services are still starting. Check logs:"
echo " ${compose_cmd[*]} ${compose_files[*]} logs"
exit 0
fi
echo ""
echo "✓ Multica is running!"
echo " Frontend: ${frontend_url}"
echo " Backend: ${backend_url}"
echo ""
if [ "$mode" = "build" ]; then
echo "Built images locally via docker-compose.selfhost.build.yml."
echo "Local tags: multica-backend:dev and multica-web:dev."
else
echo "Images: ${MULTICA_BACKEND_IMAGE:-ghcr.io/multica-ai/multica-backend}:${MULTICA_IMAGE_TAG:-latest}"
echo " ${MULTICA_WEB_IMAGE:-ghcr.io/multica-ai/multica-web}:${MULTICA_IMAGE_TAG:-latest}"
fi
echo ""
echo "Log in: configure RESEND_API_KEY in .env for email codes,"
echo " or read the generated code from backend logs when Resend is unset."
echo ""
echo "Next — install the CLI and connect your machine:"
echo " brew install multica-ai/tap/multica"
echo " multica setup self-host"