mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-20 02:27:57 +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 "$@"
|
||||
@@ -0,0 +1,702 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Seed a rich transfer-demo inventory on the current Coolify instance.
|
||||
* Run: php artisan tinker scripts/seed-transfer-demo.php
|
||||
* Or: ./scripts/dev-instances exec a php artisan tinker --execute "require 'scripts/seed-transfer-demo.php';"
|
||||
*/
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\Environment;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\GithubApp;
|
||||
use App\Models\LocalFileVolume;
|
||||
use App\Models\LocalPersistentVolume;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\ScheduledTask;
|
||||
use App\Models\ScheduledVolumeBackup;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
use App\Models\SharedEnvironmentVariable;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use App\Models\Tag;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
$teamId = 0;
|
||||
$team = Team::find($teamId) ?? Team::query()->orderBy('id')->first();
|
||||
if (! $team) {
|
||||
throw new RuntimeException('No team found.');
|
||||
}
|
||||
$teamId = $team->id;
|
||||
|
||||
$report = [];
|
||||
|
||||
// --- Private key (testing-host SSH) ---
|
||||
$keyMaterial = <<<'KEY'
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevAAAAJi/QySHv0Mk
|
||||
hwAAAAtzc2gtZWQyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevA
|
||||
AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV
|
||||
uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
KEY;
|
||||
|
||||
// Prefer existing testing-host key (same material) to avoid fingerprint uniqueness errors.
|
||||
$key = PrivateKey::where('uuid', 'transfer-full-key')->first()
|
||||
?? PrivateKey::where('name', 'Testing Host Key')->where('team_id', $teamId)->first()
|
||||
?? PrivateKey::where('team_id', $teamId)->orderBy('id')->first();
|
||||
if (! $key) {
|
||||
$key = PrivateKey::withoutEvents(function () use ($keyMaterial, $teamId) {
|
||||
$key = new PrivateKey;
|
||||
$key->forceFill([
|
||||
'name' => 'transfer-full-key',
|
||||
'description' => 'SSH key for transfer demo server (testing-host)',
|
||||
'private_key' => $keyMaterial,
|
||||
'team_id' => $teamId,
|
||||
]);
|
||||
$key->uuid = 'transfer-full-key';
|
||||
$key->save();
|
||||
|
||||
return $key;
|
||||
});
|
||||
}
|
||||
$report['private_key'] = $key->uuid;
|
||||
|
||||
// Idempotent cleanup of known demo UUIDs / leftover schedules.
|
||||
DB::table('scheduled_volume_backups')->whereIn('uuid', [
|
||||
'demo-nixpacks-vol-backup', 'demo-svc-whoami-vol-backup',
|
||||
])->delete();
|
||||
DB::table('scheduled_database_backups')->whereIn('uuid', [
|
||||
'demo-postgres-backup', 'demo-mysql-backup', 'demo-svc-db-backup',
|
||||
])->delete();
|
||||
DB::table('application_previews')->where('uuid', 'demo-preview-101')->delete();
|
||||
DB::table('local_file_volumes')->whereIn('uuid', [
|
||||
'demo-nixpacks-file', 'demo-pg-file-conf', 'demo-svc-whoami-file',
|
||||
])->delete();
|
||||
DB::table('github_apps')->where('uuid', 'transfer-team-github')->delete();
|
||||
|
||||
// Clean prior demo server if re-running (query builder avoids decrypting unrelated rows).
|
||||
$old = Server::withTrashed()->where('uuid', 'transfer-full-demo')->first();
|
||||
if ($old) {
|
||||
$destIds = StandaloneDocker::where('server_id', $old->id)->pluck('id')->all();
|
||||
DB::table('additional_destinations')->where('server_id', $old->id)->delete();
|
||||
|
||||
$appIds = Application::withTrashed()
|
||||
->where('destination_type', StandaloneDocker::class)
|
||||
->whereIn('destination_id', $destIds ?: [0])
|
||||
->pluck('id');
|
||||
if ($appIds->isNotEmpty()) {
|
||||
ApplicationPreview::withTrashed()->whereIn('application_id', $appIds)->forceDelete();
|
||||
ScheduledTask::whereIn('application_id', $appIds)->delete();
|
||||
Application::withTrashed()->whereIn('id', $appIds)->forceDelete();
|
||||
}
|
||||
|
||||
$serviceIds = Service::withTrashed()->where('server_id', $old->id)->pluck('id');
|
||||
if ($serviceIds->isNotEmpty()) {
|
||||
ServiceApplication::withTrashed()->whereIn('service_id', $serviceIds)->forceDelete();
|
||||
ServiceDatabase::withTrashed()->whereIn('service_id', $serviceIds)->forceDelete();
|
||||
ScheduledTask::whereIn('service_id', $serviceIds)->delete();
|
||||
Service::withTrashed()->whereIn('id', $serviceIds)->forceDelete();
|
||||
}
|
||||
|
||||
$dbTables = [
|
||||
'standalone_postgresqls',
|
||||
'standalone_mysqls',
|
||||
'standalone_redis',
|
||||
'standalone_mongodbs',
|
||||
];
|
||||
foreach ($dbTables as $table) {
|
||||
if (! Schema::hasTable($table)) {
|
||||
continue;
|
||||
}
|
||||
$ids = DB::table($table)
|
||||
->where('destination_type', StandaloneDocker::class)
|
||||
->whereIn('destination_id', $destIds ?: [0])
|
||||
->pluck('id');
|
||||
if ($ids->isNotEmpty()) {
|
||||
DB::table('scheduled_database_backups')
|
||||
->whereIn('database_id', $ids)
|
||||
->delete();
|
||||
DB::table($table)->whereIn('id', $ids)->delete();
|
||||
}
|
||||
}
|
||||
|
||||
DB::table('local_persistent_volumes')
|
||||
->whereIn('resource_id', $destIds ?: [0])
|
||||
->delete();
|
||||
SslCertificate::where('server_id', $old->id)->delete();
|
||||
SharedEnvironmentVariable::where('server_id', $old->id)->delete();
|
||||
StandaloneDocker::where('server_id', $old->id)->delete();
|
||||
$old->forceDelete();
|
||||
}
|
||||
|
||||
// --- Server ---
|
||||
$server = Server::create([
|
||||
'name' => 'transfer-full-demo',
|
||||
'description' => 'Full inventory for A→B server transfer testing',
|
||||
'ip' => 'testing-host',
|
||||
'user' => 'root',
|
||||
'port' => 22,
|
||||
'team_id' => $teamId,
|
||||
'private_key_id' => $key->id,
|
||||
]);
|
||||
$server->uuid = 'transfer-full-demo';
|
||||
$server->save();
|
||||
$server->settings->forceFill([
|
||||
'is_reachable' => true,
|
||||
'is_usable' => true,
|
||||
'wildcard_domain' => 'https://demo.transfer.local',
|
||||
])->save();
|
||||
$report['server'] = $server->uuid;
|
||||
|
||||
// --- Destinations ---
|
||||
$dest = StandaloneDocker::where('server_id', $server->id)->first()
|
||||
?? StandaloneDocker::create([
|
||||
'name' => 'coolify',
|
||||
'network' => 'coolify',
|
||||
'server_id' => $server->id,
|
||||
]);
|
||||
$dest->uuid = 'transfer-full-dest';
|
||||
$dest->saveQuietly();
|
||||
$report['destination'] = $dest->uuid;
|
||||
|
||||
// --- Project / environments ---
|
||||
$project = Project::firstOrCreate(
|
||||
['name' => 'Transfer Full Demo', 'team_id' => $teamId],
|
||||
['description' => 'Resources to migrate between Coolify instances']
|
||||
);
|
||||
$project->uuid = $project->uuid ?: new_public_id();
|
||||
$production = $project->environments()->where('name', 'production')->first()
|
||||
?? Environment::create(['name' => 'production', 'project_id' => $project->id, 'description' => 'prod']);
|
||||
$staging = $project->environments()->where('name', 'staging')->first()
|
||||
?? Environment::create(['name' => 'staging', 'project_id' => $project->id, 'description' => 'staging']);
|
||||
$report['project'] = $project->uuid ?? $project->name;
|
||||
$report['environments'] = [$production->name, $staging->name];
|
||||
|
||||
// Shared env vars
|
||||
SharedEnvironmentVariable::updateOrCreate(
|
||||
['type' => 'server', 'server_id' => $server->id, 'key' => 'DEMO_SERVER_TOKEN'],
|
||||
['value' => 'server-shared-secret', 'team_id' => $teamId, 'is_literal' => true, 'comment' => 'server-level']
|
||||
);
|
||||
SharedEnvironmentVariable::updateOrCreate(
|
||||
['type' => 'project', 'project_id' => $project->id, 'key' => 'DEMO_PROJECT_API'],
|
||||
['value' => 'project-shared-secret', 'team_id' => $teamId, 'is_literal' => true]
|
||||
);
|
||||
SharedEnvironmentVariable::updateOrCreate(
|
||||
['type' => 'environment', 'environment_id' => $production->id, 'key' => 'DEMO_ENV_FLAG'],
|
||||
['value' => 'production', 'team_id' => $teamId, 'is_literal' => true]
|
||||
);
|
||||
|
||||
// Tags
|
||||
$tagCritical = Tag::firstOrCreate(['name' => 'critical', 'team_id' => $teamId], ['uuid' => new_public_id()]);
|
||||
$tagDemo = Tag::firstOrCreate(['name' => 'transfer-demo', 'team_id' => $teamId], ['uuid' => new_public_id()]);
|
||||
|
||||
// Team-scoped GitHub App (not system-wide)
|
||||
$ghApp = GithubApp::where('uuid', 'transfer-team-github')->first();
|
||||
if (! $ghApp) {
|
||||
$ghApp = new GithubApp;
|
||||
$ghApp->forceFill([
|
||||
'name' => 'Transfer Team GitHub',
|
||||
'api_url' => 'https://api.github.com',
|
||||
'html_url' => 'https://github.com',
|
||||
'is_public' => false,
|
||||
'is_system_wide' => false,
|
||||
'team_id' => $teamId,
|
||||
'app_id' => 99001,
|
||||
'installation_id' => 88001,
|
||||
'client_id' => 'Iv1.transfer-demo',
|
||||
'client_secret' => 'gh-client-secret-demo',
|
||||
'webhook_secret' => 'gh-webhook-secret-demo',
|
||||
'private_key_id' => $key->id,
|
||||
]);
|
||||
$ghApp->uuid = 'transfer-team-github';
|
||||
$ghApp->save();
|
||||
}
|
||||
$report['github_app'] = $ghApp->uuid;
|
||||
|
||||
// ========== APPLICATIONS ==========
|
||||
// 1) Nixpacks git app with GH source
|
||||
$appNix = Application::create([
|
||||
'name' => 'demo-nixpacks-app',
|
||||
'git_repository' => 'https://github.com/coollabsio/coolify-examples',
|
||||
'git_branch' => 'nodejs',
|
||||
'build_pack' => 'nixpacks',
|
||||
'ports_exposes' => '3000',
|
||||
'fqdn' => 'https://nixpacks.demo.transfer.local',
|
||||
'environment_id' => $production->id,
|
||||
'destination_id' => $dest->id,
|
||||
'destination_type' => $dest->getMorphClass(),
|
||||
'source_type' => GithubApp::class,
|
||||
'source_id' => $ghApp->id,
|
||||
'status' => 'exited',
|
||||
'description' => 'Nixpacks app with GitHub App source',
|
||||
]);
|
||||
$appNix->uuid = 'demo-nixpacks-app';
|
||||
$appNix->save();
|
||||
$appNix->tags()->syncWithoutDetaching([$tagDemo->id, $tagCritical->id]);
|
||||
if ($appNix->settings) {
|
||||
$appNix->settings->forceFill(['is_auto_deploy_enabled' => true, 'is_force_https_enabled' => true])->save();
|
||||
}
|
||||
EnvironmentVariable::withoutEvents(function () use ($appNix) {
|
||||
$e = new EnvironmentVariable;
|
||||
$e->forceFill([
|
||||
'key' => 'APP_SECRET',
|
||||
'value' => 'nixpacks-app-secret-value',
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $appNix->id,
|
||||
'is_runtime' => true,
|
||||
'is_buildtime' => true,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
$e->uuid = new_public_id();
|
||||
$e->save();
|
||||
$e2 = new EnvironmentVariable;
|
||||
$e2->forceFill([
|
||||
'key' => 'PREVIEW_ONLY',
|
||||
'value' => 'preview-secret',
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $appNix->id,
|
||||
'is_runtime' => true,
|
||||
'is_buildtime' => false,
|
||||
'is_preview' => true,
|
||||
]);
|
||||
$e2->uuid = new_public_id();
|
||||
$e2->save();
|
||||
});
|
||||
$volNix = new LocalPersistentVolume;
|
||||
$volNix->forceFill([
|
||||
'name' => 'demo-nixpacks-data',
|
||||
'mount_path' => '/app/data',
|
||||
'resource_type' => $appNix->getMorphClass(),
|
||||
'resource_id' => $appNix->id,
|
||||
]);
|
||||
$volNix->uuid = new_public_id();
|
||||
$volNix->save();
|
||||
LocalFileVolume::withoutEvents(function () use ($appNix) {
|
||||
$f = new LocalFileVolume;
|
||||
$f->forceFill([
|
||||
'fs_path' => './config.json',
|
||||
'mount_path' => '/app/config.json',
|
||||
'content' => '{"demo":true,"from":"transfer-seed"}',
|
||||
'is_directory' => false,
|
||||
'resource_type' => $appNix->getMorphClass(),
|
||||
'resource_id' => $appNix->id,
|
||||
]);
|
||||
$f->uuid = 'demo-nixpacks-file';
|
||||
$f->save();
|
||||
});
|
||||
ScheduledTask::create([
|
||||
'uuid' => new_public_id(),
|
||||
'name' => 'nightly-cleanup',
|
||||
'command' => 'php artisan cache:clear',
|
||||
'frequency' => '0 3 * * *',
|
||||
'application_id' => $appNix->id,
|
||||
'team_id' => $teamId,
|
||||
'enabled' => true,
|
||||
]);
|
||||
ScheduledVolumeBackup::create([
|
||||
'uuid' => 'demo-nixpacks-vol-backup',
|
||||
'backupable_type' => $volNix->getMorphClass(),
|
||||
'backupable_id' => $volNix->id,
|
||||
'team_id' => $teamId,
|
||||
'frequency' => '0 4 * * 0',
|
||||
'enabled' => true,
|
||||
'save_s3' => false,
|
||||
]);
|
||||
$preview = new ApplicationPreview;
|
||||
$preview->forceFill([
|
||||
'application_id' => $appNix->id,
|
||||
'pull_request_id' => 101,
|
||||
'pull_request_html_url' => 'https://github.com/coollabsio/coolify-examples/pull/101',
|
||||
'fqdn' => 'https://pr-101.nixpacks.demo.transfer.local',
|
||||
'status' => 'exited',
|
||||
'git_type' => 'github',
|
||||
]);
|
||||
$preview->uuid = 'demo-preview-101';
|
||||
$preview->save();
|
||||
$previewVol = new LocalPersistentVolume;
|
||||
$previewVol->forceFill([
|
||||
'name' => 'demo-preview-101-data',
|
||||
'mount_path' => '/app/data',
|
||||
'resource_type' => $preview->getMorphClass(),
|
||||
'resource_id' => $preview->id,
|
||||
]);
|
||||
$previewVol->uuid = new_public_id();
|
||||
$previewVol->save();
|
||||
|
||||
// 2) Dockerfile app
|
||||
$appDocker = Application::create([
|
||||
'name' => 'demo-dockerfile-app',
|
||||
'git_repository' => 'https://github.com/coollabsio/coolify-examples',
|
||||
'git_branch' => 'main',
|
||||
'build_pack' => 'dockerfile',
|
||||
'ports_exposes' => '80',
|
||||
'fqdn' => 'https://dockerfile.demo.transfer.local',
|
||||
'environment_id' => $production->id,
|
||||
'destination_id' => $dest->id,
|
||||
'destination_type' => $dest->getMorphClass(),
|
||||
'dockerfile' => "FROM nginx:alpine\nEXPOSE 80\n",
|
||||
'status' => 'exited',
|
||||
]);
|
||||
$appDocker->uuid = 'demo-dockerfile-app';
|
||||
$appDocker->save();
|
||||
$appDocker->tags()->syncWithoutDetaching([$tagDemo->id]);
|
||||
|
||||
// 3) Docker image / static-ish
|
||||
$appImage = Application::create([
|
||||
'name' => 'demo-image-app',
|
||||
'git_repository' => 'coollabsio/coolify-examples',
|
||||
'git_branch' => 'main',
|
||||
'build_pack' => 'dockercompose',
|
||||
'ports_exposes' => '80',
|
||||
'fqdn' => 'https://compose.demo.transfer.local',
|
||||
'environment_id' => $staging->id,
|
||||
'destination_id' => $dest->id,
|
||||
'destination_type' => $dest->getMorphClass(),
|
||||
'docker_compose_raw' => "services:\n web:\n image: traefik/whoami\n ports:\n - '80'\n",
|
||||
'docker_compose' => "services:\n web:\n image: traefik/whoami\n ports:\n - '80'\n",
|
||||
'status' => 'exited',
|
||||
]);
|
||||
$appImage->uuid = 'demo-compose-app';
|
||||
$appImage->save();
|
||||
|
||||
// 4) Deploy-key style app (private_key on application)
|
||||
$appDeployKey = Application::create([
|
||||
'name' => 'demo-deploykey-app',
|
||||
'git_repository' => 'git@github.com:example/private-app.git',
|
||||
'git_branch' => 'main',
|
||||
'build_pack' => 'nixpacks',
|
||||
'ports_exposes' => '3000',
|
||||
'environment_id' => $production->id,
|
||||
'destination_id' => $dest->id,
|
||||
'destination_type' => $dest->getMorphClass(),
|
||||
'private_key_id' => $key->id,
|
||||
'status' => 'exited',
|
||||
]);
|
||||
$appDeployKey->uuid = 'demo-deploykey-app';
|
||||
$appDeployKey->save();
|
||||
|
||||
$report['applications'] = [
|
||||
$appNix->uuid,
|
||||
$appDocker->uuid,
|
||||
$appImage->uuid,
|
||||
$appDeployKey->uuid,
|
||||
];
|
||||
|
||||
// ========== DATABASES ==========
|
||||
$mkDb = function (string $class, string $uuid, string $name, array $extra) use ($production, $dest, $tagCritical, $teamId) {
|
||||
return $class::withoutEvents(function () use ($class, $uuid, $name, $extra, $production, $dest, $tagCritical, $teamId) {
|
||||
$db = new $class;
|
||||
$db->forceFill(array_merge([
|
||||
'name' => $name,
|
||||
'environment_id' => $production->id,
|
||||
'destination_id' => $dest->id,
|
||||
'destination_type' => $dest->getMorphClass(),
|
||||
'status' => 'exited',
|
||||
], $extra));
|
||||
$db->uuid = $uuid;
|
||||
$db->save();
|
||||
if (method_exists($db, 'tags')) {
|
||||
$db->tags()->syncWithoutDetaching([$tagCritical->id]);
|
||||
}
|
||||
if (method_exists($db, 'persistentStorages')) {
|
||||
$vol = new LocalPersistentVolume;
|
||||
$vol->forceFill([
|
||||
'name' => $uuid.'-data',
|
||||
'mount_path' => match ($class) {
|
||||
StandalonePostgresql::class => '/var/lib/postgresql/data',
|
||||
StandaloneMysql::class => '/var/lib/mysql',
|
||||
StandaloneRedis::class => '/data',
|
||||
StandaloneMongodb::class => '/data/db',
|
||||
default => '/data',
|
||||
},
|
||||
'resource_type' => $db->getMorphClass(),
|
||||
'resource_id' => $db->id,
|
||||
]);
|
||||
$vol->uuid = new_public_id();
|
||||
$vol->save();
|
||||
}
|
||||
if (method_exists($db, 'fileStorages') && $class === StandalonePostgresql::class) {
|
||||
LocalFileVolume::withoutEvents(function () use ($db) {
|
||||
$f = new LocalFileVolume;
|
||||
$f->forceFill([
|
||||
'fs_path' => './pg-conf.d',
|
||||
'mount_path' => '/etc/postgresql/conf.d',
|
||||
'content' => null,
|
||||
'is_directory' => true,
|
||||
'resource_type' => $db->getMorphClass(),
|
||||
'resource_id' => $db->id,
|
||||
]);
|
||||
$f->uuid = 'demo-pg-file-conf';
|
||||
$f->save();
|
||||
});
|
||||
}
|
||||
if (method_exists($db, 'scheduledBackups') && in_array($class, [StandalonePostgresql::class, StandaloneMysql::class], true)) {
|
||||
ScheduledDatabaseBackup::create([
|
||||
'uuid' => $uuid.'-backup',
|
||||
'team_id' => $teamId,
|
||||
'enabled' => true,
|
||||
'save_s3' => false,
|
||||
'frequency' => '0 2 * * *',
|
||||
'databases_to_backup' => $extra['postgres_db'] ?? $extra['mysql_database'] ?? 'app',
|
||||
'database_type' => $db->getMorphClass(),
|
||||
'database_id' => $db->id,
|
||||
]);
|
||||
}
|
||||
EnvironmentVariable::withoutEvents(function () use ($db, $name) {
|
||||
$e = new EnvironmentVariable;
|
||||
$e->forceFill([
|
||||
'key' => 'DB_LABEL',
|
||||
'value' => $name,
|
||||
'resourceable_type' => $db::class,
|
||||
'resourceable_id' => $db->id,
|
||||
'is_runtime' => true,
|
||||
'is_buildtime' => false,
|
||||
]);
|
||||
$e->uuid = new_public_id();
|
||||
$e->save();
|
||||
});
|
||||
|
||||
return $db;
|
||||
});
|
||||
};
|
||||
|
||||
$pg = $mkDb(StandalonePostgresql::class, 'demo-postgres', 'demo-postgres', [
|
||||
'postgres_user' => 'demo',
|
||||
'postgres_password' => 'pg-secret-password',
|
||||
'postgres_db' => 'demoddb',
|
||||
]);
|
||||
$mysql = $mkDb(StandaloneMysql::class, 'demo-mysql', 'demo-mysql', [
|
||||
'mysql_root_password' => 'mysql-root-secret',
|
||||
'mysql_user' => 'demo',
|
||||
'mysql_password' => 'mysql-user-secret',
|
||||
'mysql_database' => 'demoddb',
|
||||
]);
|
||||
$redis = $mkDb(StandaloneRedis::class, 'demo-redis', 'demo-redis', [
|
||||
'image' => 'redis:7-alpine',
|
||||
]);
|
||||
// Redis password is stored as env var, not a column.
|
||||
EnvironmentVariable::withoutEvents(function () use ($redis) {
|
||||
$e = new EnvironmentVariable;
|
||||
$e->forceFill([
|
||||
'key' => 'REDIS_PASSWORD',
|
||||
'value' => 'redis-secret-password',
|
||||
'resourceable_type' => StandaloneRedis::class,
|
||||
'resourceable_id' => $redis->id,
|
||||
'is_runtime' => true,
|
||||
'is_buildtime' => false,
|
||||
]);
|
||||
$e->uuid = new_public_id();
|
||||
$e->save();
|
||||
});
|
||||
$mongo = $mkDb(StandaloneMongodb::class, 'demo-mongo', 'demo-mongo', [
|
||||
'mongo_initdb_root_username' => 'root',
|
||||
'mongo_initdb_root_password' => 'mongo-root-secret',
|
||||
'mongo_initdb_database' => 'demoddb',
|
||||
'image' => 'mongo:7',
|
||||
]);
|
||||
$report['databases'] = [$pg->uuid, $mysql->uuid, $redis->uuid, $mongo->uuid];
|
||||
|
||||
// ========== SERVICE STACK ==========
|
||||
$service = Service::create([
|
||||
'name' => 'demo-service-stack',
|
||||
'environment_id' => $production->id,
|
||||
'destination_id' => $dest->id,
|
||||
'destination_type' => $dest->getMorphClass(),
|
||||
'server_id' => $server->id,
|
||||
'docker_compose_raw' => <<<'YAML'
|
||||
services:
|
||||
whoami:
|
||||
image: traefik/whoami
|
||||
environment:
|
||||
WHOAMI_NAME: transfer-demo
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_PASSWORD: service-db-secret
|
||||
YAML,
|
||||
'docker_compose' => <<<'YAML'
|
||||
services:
|
||||
whoami:
|
||||
image: traefik/whoami
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
YAML,
|
||||
]);
|
||||
$service->uuid = 'demo-service-stack';
|
||||
$service->save();
|
||||
$service->tags()->syncWithoutDetaching([$tagDemo->id]);
|
||||
|
||||
EnvironmentVariable::withoutEvents(function () use ($service) {
|
||||
$e = new EnvironmentVariable;
|
||||
$e->forceFill([
|
||||
'key' => 'SERVICE_TOKEN',
|
||||
'value' => 'service-level-secret',
|
||||
'resourceable_type' => Service::class,
|
||||
'resourceable_id' => $service->id,
|
||||
'is_runtime' => true,
|
||||
]);
|
||||
$e->uuid = new_public_id();
|
||||
$e->save();
|
||||
});
|
||||
ScheduledTask::create([
|
||||
'uuid' => new_public_id(),
|
||||
'name' => 'service-ping',
|
||||
'command' => 'echo ping',
|
||||
'frequency' => '*/30 * * * *',
|
||||
'service_id' => $service->id,
|
||||
'team_id' => $teamId,
|
||||
'enabled' => true,
|
||||
'container' => 'whoami',
|
||||
]);
|
||||
|
||||
$svcApp = new ServiceApplication;
|
||||
$svcApp->forceFill([
|
||||
'service_id' => $service->id,
|
||||
'name' => 'whoami',
|
||||
'human_name' => 'Whoami',
|
||||
'description' => 'Nested service app',
|
||||
'fqdn' => 'https://whoami.demo.transfer.local',
|
||||
'image' => 'traefik/whoami:latest',
|
||||
'ports' => '80',
|
||||
'exposes' => '80',
|
||||
'required_fqdn' => true,
|
||||
'is_gzip_enabled' => true,
|
||||
'is_stripprefix_enabled' => true,
|
||||
'status' => 'exited',
|
||||
]);
|
||||
$svcApp->uuid = 'demo-svc-whoami';
|
||||
$svcApp->save();
|
||||
EnvironmentVariable::withoutEvents(function () use ($svcApp) {
|
||||
$e = new EnvironmentVariable;
|
||||
$e->forceFill([
|
||||
'key' => 'WHOAMI_NAME',
|
||||
'value' => 'nested-whoami-secret',
|
||||
'resourceable_type' => ServiceApplication::class,
|
||||
'resourceable_id' => $svcApp->id,
|
||||
'is_runtime' => true,
|
||||
]);
|
||||
$e->uuid = new_public_id();
|
||||
$e->save();
|
||||
});
|
||||
$svcAppVol = new LocalPersistentVolume;
|
||||
$svcAppVol->forceFill([
|
||||
'name' => 'demo-svc-whoami-data',
|
||||
'mount_path' => '/data',
|
||||
'resource_type' => $svcApp->getMorphClass(),
|
||||
'resource_id' => $svcApp->id,
|
||||
]);
|
||||
$svcAppVol->uuid = new_public_id();
|
||||
$svcAppVol->save();
|
||||
LocalFileVolume::withoutEvents(function () use ($svcApp) {
|
||||
$f = new LocalFileVolume;
|
||||
$f->forceFill([
|
||||
'fs_path' => './whoami.env',
|
||||
'mount_path' => '/whoami.env',
|
||||
'content' => "NAME=transfer\n",
|
||||
'is_directory' => false,
|
||||
'resource_type' => $svcApp->getMorphClass(),
|
||||
'resource_id' => $svcApp->id,
|
||||
]);
|
||||
$f->uuid = 'demo-svc-whoami-file';
|
||||
$f->save();
|
||||
});
|
||||
ScheduledVolumeBackup::create([
|
||||
'uuid' => 'demo-svc-whoami-vol-backup',
|
||||
'backupable_type' => $svcAppVol->getMorphClass(),
|
||||
'backupable_id' => $svcAppVol->id,
|
||||
'team_id' => $teamId,
|
||||
'frequency' => '0 5 * * *',
|
||||
'enabled' => true,
|
||||
'save_s3' => false,
|
||||
]);
|
||||
|
||||
$svcDb = new ServiceDatabase;
|
||||
$svcDb->forceFill([
|
||||
'service_id' => $service->id,
|
||||
'name' => 'db',
|
||||
'human_name' => 'Service Postgres',
|
||||
'image' => 'postgres:16-alpine',
|
||||
'ports' => '5432',
|
||||
'exposes' => '5432',
|
||||
'public_port' => 15432,
|
||||
'is_public' => false,
|
||||
'custom_type' => 'postgresql',
|
||||
'status' => 'exited',
|
||||
]);
|
||||
$svcDb->uuid = 'demo-svc-db';
|
||||
$svcDb->save();
|
||||
$svcDbVol = new LocalPersistentVolume;
|
||||
$svcDbVol->forceFill([
|
||||
'name' => 'demo-svc-db-data',
|
||||
'mount_path' => '/var/lib/postgresql/data',
|
||||
'resource_type' => $svcDb->getMorphClass(),
|
||||
'resource_id' => $svcDb->id,
|
||||
]);
|
||||
$svcDbVol->uuid = new_public_id();
|
||||
$svcDbVol->save();
|
||||
ScheduledDatabaseBackup::create([
|
||||
'uuid' => 'demo-svc-db-backup',
|
||||
'team_id' => $teamId,
|
||||
'enabled' => true,
|
||||
'save_s3' => false,
|
||||
'frequency' => '0 1 * * *',
|
||||
'databases_to_backup' => 'postgres',
|
||||
'database_type' => $svcDb->getMorphClass(),
|
||||
'database_id' => $svcDb->id,
|
||||
]);
|
||||
|
||||
$report['service'] = $service->uuid;
|
||||
$report['service_applications'] = [$svcApp->uuid];
|
||||
$report['service_databases'] = [$svcDb->uuid];
|
||||
|
||||
// ========== SSL cert (server-level) ==========
|
||||
SslCertificate::create([
|
||||
'ssl_certificate' => "-----BEGIN CERTIFICATE-----\nMIIDemoTransferCertPlaceholder\n-----END CERTIFICATE-----\n",
|
||||
'ssl_private_key' => "-----BEGIN PRIVATE KEY-----\nMIIDemoTransferKeyPlaceholder\n-----END PRIVATE KEY-----\n",
|
||||
'configuration_dir' => '/data/coolify/proxy',
|
||||
'mount_path' => '/etc/ssl/certs',
|
||||
'common_name' => 'demo.transfer.local',
|
||||
'valid_until' => now()->addYear(),
|
||||
'is_ca_certificate' => false,
|
||||
'server_id' => $server->id,
|
||||
'resource_type' => null,
|
||||
'resource_id' => null,
|
||||
]);
|
||||
$report['ssl_certificates'] = 1;
|
||||
|
||||
// Guarantee no additional destinations
|
||||
$extraDestCount = DB::table('additional_destinations')->where('server_id', $server->id)->count();
|
||||
$report['additional_destinations'] = $extraDestCount;
|
||||
|
||||
$report['counts'] = [
|
||||
'applications' => Application::whereIn('uuid', $report['applications'])->count(),
|
||||
'databases' => 4,
|
||||
'services' => 1,
|
||||
'service_apps' => ServiceApplication::where('service_id', $service->id)->count(),
|
||||
'service_dbs' => ServiceDatabase::where('service_id', $service->id)->count(),
|
||||
'scheduled_tasks' => ScheduledTask::where('application_id', $appNix->id)->orWhere('service_id', $service->id)->count(),
|
||||
'scheduled_db_backups' => ScheduledDatabaseBackup::whereIn('uuid', [
|
||||
'demo-postgres-backup', 'demo-mysql-backup', 'demo-svc-db-backup',
|
||||
])->count(),
|
||||
'volume_backups' => ScheduledVolumeBackup::whereIn('uuid', [
|
||||
'demo-nixpacks-vol-backup', 'demo-svc-whoami-vol-backup',
|
||||
])->count(),
|
||||
'previews' => ApplicationPreview::where('uuid', 'demo-preview-101')->count(),
|
||||
'tags' => $appNix->tags()->count(),
|
||||
];
|
||||
|
||||
echo json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)."\n";
|
||||
Reference in New Issue
Block a user