#!/usr/bin/env bash # Two local Coolify instances (isolated stacks) for multi-control-plane testing. # # Usage: # ./scripts/dev-instances up # start a + b # ./scripts/dev-instances up a # start a only # ./scripts/dev-instances up a --with vite # HMR only for a single instance # ./scripts/dev-instances down # stop a + b # ./scripts/dev-instances urls # ./scripts/dev-instances ps # ./scripts/dev-instances logs a # ./scripts/dev-instances exec a php artisan migrate --force # # Ports (fixed): # a app 8000 db 5432 redis 6379 soketi 6001/6002 flux 6443 # b app 8001 db 5433 redis 6380 soketi 6011/6012 flux 6444 # # Frontend: multi-instance uses npm run build (shared public/build). Do not use # two Vite HMR servers — public/hot is a single shared file. # # Compose: docker-compose.dev-multi.yml # Env: .dev-instances/{a,b}.env (generated, gitignored) # set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" COMPOSE_FILE="docker-compose.dev-multi.yml" ENV_DIR=".dev-instances" INSTANCES_ALL=(a b) usage() { sed -n '2,18p' "$0" | sed 's/^# \?//' exit "${1:-0}" } # Fixed port offsets: a=0, b=1 instance_offset() { case "$1" in a|1) echo 0 ;; b|2) echo 1 ;; *) echo "Unknown instance '$1' (use a or b)" >&2 exit 1 ;; esac } normalize_name() { case "$1" in a|1) echo a ;; b|2) echo b ;; *) echo "Unknown instance '$1' (use a or b)" >&2 exit 1 ;; esac } project_name() { echo "coolify-$1"; } env_file() { echo "${ENV_DIR}/$1.env"; } ensure_env() { local name="$1" local offset env_file app_port existing_key key offset="$(instance_offset "$name")" env_file="$(env_file "$name")" app_port=$((8000 + offset)) mkdir -p "$ENV_DIR" # Never rotate APP_KEY once set: encrypted DB columns (private keys, secrets) # become unreadable ("The MAC is invalid") if APP_KEY changes while volumes persist. existing_key="" if [[ -f "$env_file" ]]; then existing_key="$(grep -E '^APP_KEY=base64:' "$env_file" 2>/dev/null | tail -n1 | cut -d= -f2- || true)" fi if [[ -z "$existing_key" && -f ".dev-instances/${name}.appkey" ]]; then existing_key="$(cat ".dev-instances/${name}.appkey" 2>/dev/null || true)" fi if [[ -z "$existing_key" ]]; then existing_key="base64:$(openssl rand -base64 32)" fi printf '%s\n' "$existing_key" >".dev-instances/${name}.appkey" cat >"$env_file" <&2 return 1 } return fi # Rebuild if CSS entry is missing from disk (stale/partial build). local css css="$(php -r ' $m=@json_decode(@file_get_contents("public/build/manifest.json"), true); $f=$m["resources/css/app.css"]["file"] ?? ""; echo $f && is_file("public/build/".$f) ? "ok" : "missing"; ' 2>/dev/null || echo missing)" if [[ "$css" != "ok" ]]; then echo "Frontend build incomplete — running npm run build..." npm run build || echo "Warning: npm run build failed." >&2 fi } # Vite profile is only useful for a single instance; drop it when starting both. filter_vite_profile() { local -a kept=() local p want_vite=0 for p in "${PROFILES[@]:-}"; do if [[ "$p" == "vite" ]]; then want_vite=1 continue fi kept+=("$p") done if [[ $want_vite -eq 1 ]]; then if [[ ${#NAMES[@]} -gt 1 ]]; then echo "Note: ignoring --with vite (shared public/hot cannot serve two instances)." echo " Frontend uses npm run build. For HMR, run a single instance: $0 up a --with vite" else kept+=("vite") fi fi PROFILES=("${kept[@]}") } cmd_up() { parse_args "$@" filter_vite_profile # Prefer built assets before containers serve HTML. ensure_frontend_assets || true local name local -a saved_profiles=("${PROFILES[@]:-}") for name in "${NAMES[@]}"; do PROFILES=("${saved_profiles[@]}") # Only the first (only) instance may run the vite profile. if [[ ${#NAMES[@]} -eq 1 ]] && printf '%s\n' "${PROFILES[@]:-}" | grep -qx vite; then : else local -a no_vite=() local p for p in "${PROFILES[@]:-}"; do [[ "$p" != "vite" ]] && no_vite+=("$p") done PROFILES=("${no_vite[@]}") fi echo "==> Starting ${name} → http://localhost:$((8000 + $(instance_offset "$name")))" compose "$name" up -d --build done # If a lone instance started vite, keep public/hot; otherwise force built assets. if [[ ${#NAMES[@]} -gt 1 ]] || ! printf '%s\n' "${saved_profiles[@]:-}" | grep -qx vite; then ensure_frontend_assets || true fi echo cmd_urls "${NAMES[@]}" echo echo "Frontend: production build (public/build). Both instances share the same assets." } cmd_down() { parse_args "$@" local name for name in "${NAMES[@]}"; do if [[ ! -f "$(env_file "$name")" ]]; then ensure_env "$name" >/dev/null fi echo "==> Stopping ${name}" compose "$name" down --remove-orphans done } cmd_ps() { parse_args "$@" local name for name in "${NAMES[@]}"; do [[ -f "$(env_file "$name")" ]] || ensure_env "$name" >/dev/null echo "==> $(project_name "$name")" compose "$name" ps echo done } cmd_urls() { local names=("$@") if [[ ${#names[@]} -eq 0 ]]; then names=("${INSTANCES_ALL[@]}") else parse_args "$@" names=("${NAMES[@]}") fi printf '%-6s %-28s %-8s %-8s %-12s\n' "NAME" "URL" "DB" "REDIS" "SOKETI" local name envf for name in "${names[@]}"; do name="$(normalize_name "$name")" ensure_env "$name" >/dev/null envf="$(env_file "$name")" printf '%-6s %-28s %-8s %-8s %-12s\n' \ "$name" \ "$(grep -E '^APP_URL=' "$envf" | cut -d= -f2-)" \ "$(grep -E '^FORWARD_DB_PORT=' "$envf" | cut -d= -f2-)" \ "$(grep -E '^FORWARD_REDIS_PORT=' "$envf" | cut -d= -f2-)" \ "$(grep -E '^FORWARD_SOKETI_PORT=' "$envf" | cut -d= -f2-)" done } cmd_logs() { local name="${1:-}" shift || true [[ -z "$name" ]] && usage 1 name="$(normalize_name "$name")" compose "$name" logs -f "$@" } cmd_exec() { local name="${1:-}" shift || true [[ -z "$name" || $# -eq 0 ]] && usage 1 name="$(normalize_name "$name")" compose "$name" exec coolify "$@" } main() { local cmd="${1:-}" shift || true case "$cmd" in up) cmd_up "$@" ;; down) cmd_down "$@" ;; ps) cmd_ps "$@" ;; urls|ls|list) cmd_urls "$@" ;; logs) cmd_logs "$@" ;; exec) cmd_exec "$@" ;; -h|--help|help|"") usage 0 ;; *) echo "Unknown command: $cmd" >&2 usage 1 ;; esac } main "$@"