mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-20 00:23:27 +00:00
318 lines
8.7 KiB
Bash
Executable File
318 lines
8.7 KiB
Bash
Executable File
#!/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 "$@"
|