mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-20 10:23:30 +00:00
feat(servers): add cross-instance server migration (#11075)
This commit is contained in:
Executable
+317
@@ -0,0 +1,317 @@
|
||||
#!/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" <<EOF
|
||||
# Generated by scripts/dev-instances for instance ${name} — do not commit
|
||||
COMPOSE_PROJECT_NAME=coolify-${name}
|
||||
APP_NAME=Coolify-${name}
|
||||
APP_ENV=local
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost:${app_port}
|
||||
APP_KEY=${existing_key}
|
||||
APP_PORT=${app_port}
|
||||
FORWARD_DB_PORT=$((5432 + offset))
|
||||
FORWARD_REDIS_PORT=$((6379 + offset))
|
||||
FORWARD_SOKETI_PORT=$((6001 + offset * 10))
|
||||
FORWARD_SOKETI_PORT_ALT=$((6002 + offset * 10))
|
||||
FORWARD_FLUX_PORT=$((6443 + offset))
|
||||
VITE_PORT=$((5173 + offset))
|
||||
FORWARD_MAILPIT_PORT=$((1025 + offset * 100))
|
||||
FORWARD_MAILPIT_DASHBOARD_PORT=$((8025 + offset * 100))
|
||||
FORWARD_MINIO_PORT=$((9000 + offset * 10))
|
||||
FORWARD_MINIO_PORT_CONSOLE=$((9001 + offset * 10))
|
||||
DB_DATABASE=coolify
|
||||
DB_USERNAME=coolify
|
||||
DB_PASSWORD=password
|
||||
PUSHER_APP_ID=coolify-${name}
|
||||
PUSHER_APP_KEY=coolify-${name}
|
||||
PUSHER_APP_SECRET=coolify-${name}
|
||||
EOF
|
||||
}
|
||||
|
||||
compose() {
|
||||
local name="$1"
|
||||
shift
|
||||
local -a profiles=()
|
||||
local p
|
||||
for p in "${PROFILES[@]:-}"; do
|
||||
[[ -n "$p" ]] && profiles+=(--profile "$p")
|
||||
done
|
||||
ensure_env "$name"
|
||||
docker compose -p "$(project_name "$name")" -f "$COMPOSE_FILE" --env-file "$(env_file "$name")" "${profiles[@]}" "$@"
|
||||
}
|
||||
|
||||
# Parse: [a] [b] [--with vite mailpit ...]
|
||||
# Default instances when none given: a b
|
||||
parse_args() {
|
||||
NAMES=()
|
||||
PROFILES=()
|
||||
local mode=names
|
||||
local arg
|
||||
for arg in "$@"; do
|
||||
if [[ "$arg" == "--with" ]]; then
|
||||
mode=profiles
|
||||
continue
|
||||
fi
|
||||
if [[ "$mode" == "profiles" ]]; then
|
||||
PROFILES+=("$arg")
|
||||
else
|
||||
NAMES+=("$(normalize_name "$arg")")
|
||||
fi
|
||||
done
|
||||
if [[ ${#NAMES[@]} -eq 0 ]]; then
|
||||
NAMES=("${INSTANCES_ALL[@]}")
|
||||
fi
|
||||
}
|
||||
|
||||
# Multi-instance stacks share the repo bind-mount. Laravel's public/hot can only
|
||||
# point at one Vite server, so dual HMR is broken (last Vite wins → missing CSS).
|
||||
# Use a production frontend build shared by all instances instead.
|
||||
ensure_frontend_assets() {
|
||||
if [[ -f public/hot ]]; then
|
||||
echo "Removing public/hot (multi-instance cannot share Vite HMR)..."
|
||||
rm -f public/hot
|
||||
fi
|
||||
if [[ ! -d node_modules ]]; then
|
||||
echo "Installing npm dependencies..."
|
||||
npm install
|
||||
fi
|
||||
if [[ ! -f public/build/manifest.json ]]; then
|
||||
echo "Building frontend assets (npm run build)..."
|
||||
npm run build || {
|
||||
echo "Warning: npm run build failed — UI will have no CSS/JS." >&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 "$@"
|
||||
Reference in New Issue
Block a user