diff --git a/.env.production b/.env.production index fe3c8370e..cda3214d2 100644 --- a/.env.production +++ b/.env.production @@ -15,4 +15,4 @@ ROOT_USERNAME= ROOT_USER_EMAIL= ROOT_USER_PASSWORD= -REGISTRY_URL=ghcr.io +REGISTRY_URL=docker.io diff --git a/app/Actions/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php index 06abeb3a6..2f9d58797 100644 --- a/app/Actions/Server/CleanupDocker.php +++ b/app/Actions/Server/CleanupDocker.php @@ -20,7 +20,7 @@ public function handle(Server $server, bool $deleteUnusedVolumes = false, bool $ $realtimeImageWithoutPrefixVersion = "coollabsio/coolify-realtime:$realtimeImageVersion"; $helperImageVersion = getHelperVersion(); - $helperImage = config('constants.coolify.helper_image'); + $helperImage = coolifyHelperImage(); $helperImageWithVersion = "$helperImage:$helperImageVersion"; $helperImageWithoutPrefix = 'coollabsio/coolify-helper'; $helperImageWithoutPrefixVersion = "coollabsio/coolify-helper:$helperImageVersion"; diff --git a/app/Actions/Server/StartSentinel.php b/app/Actions/Server/StartSentinel.php index 289ab9ebe..6350a5f37 100644 --- a/app/Actions/Server/StartSentinel.php +++ b/app/Actions/Server/StartSentinel.php @@ -26,7 +26,7 @@ public function handle(Server $server, bool $restart = false, ?string $latestVer $endpoint = data_get($server, 'settings.sentinel_custom_url'); $debug = data_get($server, 'settings.is_sentinel_debug_enabled'); $mountDir = '/data/coolify/sentinel'; - $image = config('constants.coolify.registry_url').'/coollabsio/sentinel:'.$version; + $image = coolifyRegistryUrl().'/coollabsio/sentinel:'.$version; if (! $endpoint) { throw new \RuntimeException('You should set FQDN in Instance Settings.'); } diff --git a/app/Actions/Server/UpdateCoolify.php b/app/Actions/Server/UpdateCoolify.php index b5ebd92b2..a8c9d21e8 100644 --- a/app/Actions/Server/UpdateCoolify.php +++ b/app/Actions/Server/UpdateCoolify.php @@ -118,10 +118,14 @@ private function update() { $latestHelperImageVersion = getHelperVersion(); $upgradeScriptUrl = config('constants.coolify.upgrade_script_url'); + $registryUrl = coolifyRegistryUrl(); remote_process([ "curl -fsSL {$upgradeScriptUrl} -o /data/coolify/source/upgrade.sh", - "bash /data/coolify/source/upgrade.sh $this->latestVersion $latestHelperImageVersion", + 'bash /data/coolify/source/upgrade.sh '. + escapeshellarg($this->latestVersion).' '. + escapeshellarg($latestHelperImageVersion).' '. + escapeshellarg($registryUrl), ], $this->server); } } diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 92a9f7499..19cd8ef67 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -2124,7 +2124,7 @@ private function create_workdir() private function prepare_builder_image(bool $firstTry = true) { $this->checkForCancellation(); - $helperImage = config('constants.coolify.helper_image'); + $helperImage = coolifyHelperImage(); $helperImage = "{$helperImage}:".getHelperVersion(); // Get user home directory $this->serverUserHomeDir = instant_remote_process(['echo $HOME'], $this->server); diff --git a/app/Jobs/CleanupHelperContainersJob.php b/app/Jobs/CleanupHelperContainersJob.php index f6f5e8b5b..f1635d6d4 100644 --- a/app/Jobs/CleanupHelperContainersJob.php +++ b/app/Jobs/CleanupHelperContainersJob.php @@ -36,7 +36,7 @@ public function handle(): void 'active_deployment_uuids' => $activeDeployments, ]); - $containers = instant_remote_process_with_timeout(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("'.config('constants.coolify.registry_url').'/coollabsio/coolify-helper")))\''], $this->server, false); + $containers = instant_remote_process_with_timeout(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("'.coolifyRegistryUrl().'/coollabsio/coolify-helper")))\''], $this->server, false); $helperContainers = collect(json_decode($containers)); if ($helperContainers->count() > 0) { diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 6bc6e48db..e9e1f9105 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -739,7 +739,7 @@ private function upload_to_s3(): void private function getFullImageName(): string { - $helperImage = config('constants.coolify.helper_image'); + $helperImage = coolifyHelperImage(); $latestVersion = getHelperVersion(); return "{$helperImage}:{$latestVersion}"; diff --git a/app/Livewire/Project/Database/ImportForm.php b/app/Livewire/Project/Database/ImportForm.php index f97e78fa7..62d4e1a59 100644 --- a/app/Livewire/Project/Database/ImportForm.php +++ b/app/Livewire/Project/Database/ImportForm.php @@ -680,7 +680,7 @@ public function restoreFromS3(string $password = ''): bool|string } // Get helper image - $helperImage = config('constants.coolify.helper_image'); + $helperImage = coolifyHelperImage(); $latestVersion = getHelperVersion(); $fullImageName = "{$helperImage}:{$latestVersion}"; diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php index 9cfc4f6c8..6ed8c7ae8 100644 --- a/app/Livewire/Settings/Index.php +++ b/app/Livewire/Settings/Index.php @@ -47,8 +47,6 @@ class Index extends Component public bool $forceSaveDomains = false; - public $buildActivityId = null; - protected array $messages = [ 'fqdn.url' => 'Invalid instance URL.', 'fqdn.max' => 'URL must not exceed 255 characters.', diff --git a/app/Livewire/Settings/Updates.php b/app/Livewire/Settings/Updates.php index 88a8945d0..856b7e2a4 100644 --- a/app/Livewire/Settings/Updates.php +++ b/app/Livewire/Settings/Updates.php @@ -6,6 +6,8 @@ use App\Models\InstanceSettings; use App\Models\Server; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Illuminate\Support\Facades\Log; +use Illuminate\Validation\ValidationException; use Livewire\Attributes\Validate; use Livewire\Component; @@ -26,6 +28,9 @@ class Updates extends Component #[Validate('boolean')] public bool $is_auto_update_enabled; + #[Validate('required|string|in:docker.io,ghcr.io')] + public string $docker_registry_url; + public function mount() { if (! isInstanceAdmin()) { @@ -39,6 +44,7 @@ public function mount() $this->auto_update_frequency = $this->settings->auto_update_frequency; $this->update_check_frequency = $this->settings->update_check_frequency; $this->is_auto_update_enabled = $this->settings->is_auto_update_enabled; + $this->docker_registry_url = $this->settings->docker_registry_url ?: 'docker.io'; } public function instantSave() @@ -50,16 +56,51 @@ public function instantSave() 'auto_update_frequency' => ['required', 'string'], ]); } + $validated = $this->validate([ + 'docker_registry_url' => ['required', 'string', 'in:docker.io,ghcr.io'], + ]); $this->settings->auto_update_frequency = $this->auto_update_frequency; $this->settings->update_check_frequency = $this->update_check_frequency; $this->settings->is_auto_update_enabled = $this->is_auto_update_enabled; + $this->settings->docker_registry_url = $validated['docker_registry_url']; + $this->syncRegistryUrlToEnv($validated['docker_registry_url']); $this->settings->save(); $this->dispatch('success', 'Settings updated!'); + } catch (ValidationException $e) { + throw $e; } catch (\Exception $e) { return handleError($e, $this); } } + protected function syncRegistryUrlToEnv(string $registryUrl): void + { + if (! $this->server) { + return; + } + + try { + instant_remote_process([ + $this->registryEnvSyncCommand($registryUrl), + ], $this->server); + } catch (\Exception $e) { + Log::warning('Failed to sync REGISTRY_URL to .env', [ + 'error' => $e->getMessage(), + ]); + + throw new \RuntimeException('Failed to sync REGISTRY_URL to .env. Settings were not saved.', previous: $e); + } + } + + private function registryEnvSyncCommand(string $registryUrl): string + { + $envFile = '/data/coolify/source/.env'; + $sedExpression = escapeshellarg("s|^REGISTRY_URL=.*|REGISTRY_URL={$registryUrl}|"); + $registryLine = escapeshellarg("REGISTRY_URL={$registryUrl}"); + + return "if grep -q '^REGISTRY_URL=' {$envFile}; then sed -i {$sedExpression} {$envFile}; else printf '%s\\n' {$registryLine} >> {$envFile}; fi"; + } + public function submit() { try { @@ -89,6 +130,8 @@ public function submit() if ($this->server) { $this->server->setupDynamicProxyConfiguration(); } + } catch (ValidationException $e) { + throw $e; } catch (\Exception $e) { return handleError($e, $this); } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index ab47c067a..39c23d2bb 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -3749,6 +3749,27 @@ function redirectRoute(Component $component, string $name, array $parameters = [ return $component->redirectRoute($name, $parameters, navigate: $navigate); } +function coolifyRegistryUrl(): string +{ + try { + return instanceSettings()->docker_registry_url ?: 'docker.io'; + } catch (Throwable) { + return config('constants.coolify.registry_url', 'docker.io'); + } +} + +function coolifyHelperImage(): string +{ + $configuredHelperImage = config('constants.coolify.helper_image'); + $configuredDefaultHelperImage = config('constants.coolify.registry_url', 'docker.io').'/coollabsio/coolify-helper'; + + if ($configuredHelperImage !== $configuredDefaultHelperImage) { + return $configuredHelperImage; + } + + return coolifyRegistryUrl().'/coollabsio/coolify-helper'; +} + function getHelperVersion(): string { $settings = instanceSettings(); diff --git a/config/constants.php b/config/constants.php index 4a956b31e..15a6cc8f8 100644 --- a/config/constants.php +++ b/config/constants.php @@ -9,9 +9,9 @@ 'self_hosted' => env('SELF_HOSTED', true), 'autoupdate' => env('AUTOUPDATE'), 'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'), - 'registry_url' => env('REGISTRY_URL', 'ghcr.io'), - 'helper_image' => env('HELPER_IMAGE', env('REGISTRY_URL', 'ghcr.io').'/coollabsio/coolify-helper'), - 'realtime_image' => env('REALTIME_IMAGE', env('REGISTRY_URL', 'ghcr.io').'/coollabsio/coolify-realtime'), + 'registry_url' => env('REGISTRY_URL', 'docker.io'), + 'helper_image' => env('HELPER_IMAGE', env('REGISTRY_URL', 'docker.io').'/coollabsio/coolify-helper'), + 'realtime_image' => env('REALTIME_IMAGE', env('REGISTRY_URL', 'docker.io').'/coollabsio/coolify-realtime'), 'is_windows_docker_desktop' => env('IS_WINDOWS_DOCKER_DESKTOP', false), 'cdn_url' => env('CDN_URL', 'https://cdn.coollabs.io'), 'versions_url' => env('VERSIONS_URL', env('CDN_URL', 'https://cdn.coollabs.io').'/coolify/versions.json'), diff --git a/database/migrations/2026_03_17_073223_add_docker_registry_url_to_instance_settings.php b/database/migrations/2026_03_17_073223_add_docker_registry_url_to_instance_settings.php new file mode 100644 index 000000000..267956f47 --- /dev/null +++ b/database/migrations/2026_03_17_073223_add_docker_registry_url_to_instance_settings.php @@ -0,0 +1,25 @@ +string('docker_registry_url')->default('docker.io')->after('is_auto_update_enabled'); + }); + } + + public function down(): void + { + Schema::table('instance_settings', function (Blueprint $table) { + $table->dropColumn('docker_registry_url'); + }); + } +}; diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 8907a30b9..9618e18ec 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,6 +1,6 @@ services: coolify: - image: "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify:${LATEST_IMAGE:-latest}" + image: "${REGISTRY_URL:-docker.io}/coollabsio/coolify:${LATEST_IMAGE:-latest}" volumes: - type: bind source: /data/coolify/source/.env @@ -60,7 +60,7 @@ services: retries: 10 timeout: 2s soketi: - image: '${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.16' + image: '${REGISTRY_URL:-docker.io}/coollabsio/coolify-realtime:1.0.16' ports: - "${SOKETI_PORT:-6001}:6001" - "6002:6002" diff --git a/docker-compose.windows.yml b/docker-compose.windows.yml index da045fe03..3a6c3f11c 100644 --- a/docker-compose.windows.yml +++ b/docker-compose.windows.yml @@ -1,14 +1,14 @@ services: coolify-testing-host: init: true - image: "ghcr.io/coollabsio/coolify-testing-host:latest" + image: "docker.io/coollabsio/coolify-testing-host:latest" pull_policy: always container_name: coolify-testing-host volumes: - //var/run/docker.sock://var/run/docker.sock - ./:/data/coolify coolify: - image: "ghcr.io/coollabsio/coolify:latest" + image: "docker.io/coollabsio/coolify:latest" pull_policy: always container_name: coolify restart: always diff --git a/other/nightly/.env.production b/other/nightly/.env.production index fe3c8370e..cda3214d2 100644 --- a/other/nightly/.env.production +++ b/other/nightly/.env.production @@ -15,4 +15,4 @@ ROOT_USERNAME= ROOT_USER_EMAIL= ROOT_USER_PASSWORD= -REGISTRY_URL=ghcr.io +REGISTRY_URL=docker.io diff --git a/other/nightly/docker-compose.prod.yml b/other/nightly/docker-compose.prod.yml index 8907a30b9..9618e18ec 100644 --- a/other/nightly/docker-compose.prod.yml +++ b/other/nightly/docker-compose.prod.yml @@ -1,6 +1,6 @@ services: coolify: - image: "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify:${LATEST_IMAGE:-latest}" + image: "${REGISTRY_URL:-docker.io}/coollabsio/coolify:${LATEST_IMAGE:-latest}" volumes: - type: bind source: /data/coolify/source/.env @@ -60,7 +60,7 @@ services: retries: 10 timeout: 2s soketi: - image: '${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.16' + image: '${REGISTRY_URL:-docker.io}/coollabsio/coolify-realtime:1.0.16' ports: - "${SOKETI_PORT:-6001}:6001" - "6002:6002" diff --git a/other/nightly/docker-compose.windows.yml b/other/nightly/docker-compose.windows.yml index da045fe03..3a6c3f11c 100644 --- a/other/nightly/docker-compose.windows.yml +++ b/other/nightly/docker-compose.windows.yml @@ -1,14 +1,14 @@ services: coolify-testing-host: init: true - image: "ghcr.io/coollabsio/coolify-testing-host:latest" + image: "docker.io/coollabsio/coolify-testing-host:latest" pull_policy: always container_name: coolify-testing-host volumes: - //var/run/docker.sock://var/run/docker.sock - ./:/data/coolify coolify: - image: "ghcr.io/coollabsio/coolify:latest" + image: "docker.io/coollabsio/coolify:latest" pull_policy: always container_name: coolify restart: always diff --git a/other/nightly/install.sh b/other/nightly/install.sh index 028652d80..365e4c330 100755 --- a/other/nightly/install.sh +++ b/other/nightly/install.sh @@ -9,7 +9,7 @@ ## DOCKER_ADDRESS_POOL_SIZE - Custom Docker address pool size (default: 24) ## DOCKER_POOL_FORCE_OVERRIDE - Force override Docker address pool configuration (default: false) ## AUTOUPDATE - Set to "false" to disable auto-updates -## REGISTRY_URL - Custom registry URL for Docker images (default: ghcr.io) +## REGISTRY_URL - Custom registry URL for Docker images (default: docker.io) set -e # Exit immediately if a command exits with a non-zero status ## $1 could be empty, so we need to disable this check @@ -50,7 +50,7 @@ else REGISTRY_URL=$(grep "^REGISTRY_URL=" "$ENV_FILE" | cut -d '=' -f2) echo "Using registry URL from .env: $REGISTRY_URL" else - REGISTRY_URL="ghcr.io" + REGISTRY_URL="docker.io" echo "Using default registry URL: $REGISTRY_URL" fi fi @@ -920,9 +920,9 @@ echo -e " - Please wait." getAJoke if [[ $- == *x* ]]; then - bash -x /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-ghcr.io}" "true" + bash -x /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-docker.io}" "true" else - bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-ghcr.io}" "true" + bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-docker.io}" "true" fi echo " - Coolify installed successfully." echo " - Waiting for Coolify to be ready..." diff --git a/other/nightly/upgrade.sh b/other/nightly/upgrade.sh index 8ccacb8a0..94fb77607 100644 --- a/other/nightly/upgrade.sh +++ b/other/nightly/upgrade.sh @@ -4,9 +4,15 @@ CDN="https://cdn.coollabs.io/coolify-nightly" LATEST_IMAGE=${1:-latest} LATEST_HELPER_VERSION=${2:-latest} -REGISTRY_URL=${3:-ghcr.io} -SKIP_BACKUP=${4:-false} ENV_FILE="/data/coolify/source/.env" +if [ -n "${3+x}" ]; then + REGISTRY_URL="$3" +elif [ -f "$ENV_FILE" ] && grep -q "^REGISTRY_URL=" "$ENV_FILE"; then + REGISTRY_URL=$(grep "^REGISTRY_URL=" "$ENV_FILE" | cut -d '=' -f2- | head -n1) +else + REGISTRY_URL="docker.io" +fi +SKIP_BACKUP=${4:-false} STATUS_FILE="/data/coolify/source/.upgrade-status" DATE=$(date +%Y-%m-%d-%H-%M-%S) @@ -80,7 +86,7 @@ fi # Get all unique images from docker compose config # LATEST_IMAGE env var is needed for image substitution in compose files -IMAGES=$(LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file "$ENV_FILE" $COMPOSE_FILES config --images 2>/dev/null | sort -u) +IMAGES=$(REGISTRY_URL=${REGISTRY_URL} LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file "$ENV_FILE" $COMPOSE_FILES config --images 2>/dev/null | sort -u) if [ -z "$IMAGES" ]; then log "ERROR: Failed to extract images from docker-compose files" @@ -127,7 +133,21 @@ update_env_var() { fi } +set_env_var() { + local key="$1" + local value="$2" + + if grep -q "^${key}=" "$ENV_FILE"; then + sed -i "s|^${key}=.*|${key}=${value}|" "$ENV_FILE" + log "Updated ${key}" + else + printf '%s=%s\n' "$key" "$value" >>"$ENV_FILE" + log "Added ${key}" + fi +} + log "Checking environment variables..." +set_env_var "REGISTRY_URL" "$REGISTRY_URL" update_env_var "PUSHER_APP_ID" "$(openssl rand -hex 32)" update_env_var "PUSHER_APP_KEY" "$(openssl rand -hex 32)" update_env_var "PUSHER_APP_SECRET" "$(openssl rand -hex 32)" @@ -164,7 +184,7 @@ echo "3/6 Pulling Docker images..." echo " This may take a few minutes depending on your connection." # Also pull the helper image (not in compose files but needed for upgrade) -HELPER_IMAGE="${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}" +HELPER_IMAGE="${REGISTRY_URL:-docker.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}" echo " - Pulling $HELPER_IMAGE..." log "Pulling image: $HELPER_IMAGE" if docker pull "$HELPER_IMAGE" >>"$LOGFILE" 2>&1; then @@ -256,7 +276,7 @@ nohup bash -c " fi log 'Running docker compose up...' - docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env \${COMPOSE_FILES} up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1 + docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-docker.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env \${COMPOSE_FILES} up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1 log 'Docker compose up completed' # Final log entry diff --git a/resources/views/livewire/settings/index.blade.php b/resources/views/livewire/settings/index.blade.php index 5a6319bc8..8ffa266ee 100644 --- a/resources/views/livewire/settings/index.blade.php +++ b/resources/views/livewire/settings/index.blade.php @@ -81,12 +81,6 @@ class="px-4 py-2 text-gray-800 cursor-pointer hover:bg-gray-100 dark:hover:bg-co placeholder="2001:db8::1" autocomplete="new-password" /> - @if($buildActivityId) -
- -
- @endif @if(isDev()) @endif + +

Docker Registry

+
+ + + + +
diff --git a/scripts/install.sh b/scripts/install.sh index 430dd9d83..7569efcc8 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -9,7 +9,7 @@ ## DOCKER_ADDRESS_POOL_SIZE - Custom Docker address pool size (default: 24) ## DOCKER_POOL_FORCE_OVERRIDE - Force override Docker address pool configuration (default: false) ## AUTOUPDATE - Set to "false" to disable auto-updates -## REGISTRY_URL - Custom registry URL for Docker images (default: ghcr.io) +## REGISTRY_URL - Custom registry URL for Docker images (default: docker.io) set -e # Exit immediately if a command exits with a non-zero status ## $1 could be empty, so we need to disable this check @@ -50,7 +50,7 @@ else REGISTRY_URL=$(grep "^REGISTRY_URL=" "$ENV_FILE" | cut -d '=' -f2) echo "Using registry URL from .env: $REGISTRY_URL" else - REGISTRY_URL="ghcr.io" + REGISTRY_URL="docker.io" echo "Using default registry URL: $REGISTRY_URL" fi fi @@ -920,9 +920,9 @@ echo -e " - Please wait." getAJoke if [[ $- == *x* ]]; then - bash -x /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-ghcr.io}" "true" + bash -x /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-docker.io}" "true" else - bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-ghcr.io}" "true" + bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-docker.io}" "true" fi echo " - Coolify installed successfully." echo " - Waiting for Coolify to be ready..." diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index fb27694b0..a909d2e49 100644 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh @@ -4,9 +4,15 @@ CDN="https://cdn.coollabs.io/coolify" LATEST_IMAGE=${1:-latest} LATEST_HELPER_VERSION=${2:-latest} -REGISTRY_URL=${3:-ghcr.io} -SKIP_BACKUP=${4:-false} ENV_FILE="/data/coolify/source/.env" +if [ -n "${3+x}" ]; then + REGISTRY_URL="$3" +elif [ -f "$ENV_FILE" ] && grep -q "^REGISTRY_URL=" "$ENV_FILE"; then + REGISTRY_URL=$(grep "^REGISTRY_URL=" "$ENV_FILE" | cut -d '=' -f2- | head -n1) +else + REGISTRY_URL="docker.io" +fi +SKIP_BACKUP=${4:-false} STATUS_FILE="/data/coolify/source/.upgrade-status" DATE=$(date +%Y-%m-%d-%H-%M-%S) @@ -80,7 +86,7 @@ fi # Get all unique images from docker compose config # LATEST_IMAGE env var is needed for image substitution in compose files -IMAGES=$(LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file "$ENV_FILE" $COMPOSE_FILES config --images 2>/dev/null | sort -u) +IMAGES=$(REGISTRY_URL=${REGISTRY_URL} LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file "$ENV_FILE" $COMPOSE_FILES config --images 2>/dev/null | sort -u) if [ -z "$IMAGES" ]; then log "ERROR: Failed to extract images from docker-compose files" @@ -127,7 +133,21 @@ update_env_var() { fi } +set_env_var() { + local key="$1" + local value="$2" + + if grep -q "^${key}=" "$ENV_FILE"; then + sed -i "s|^${key}=.*|${key}=${value}|" "$ENV_FILE" + log "Updated ${key}" + else + printf '%s=%s\n' "$key" "$value" >>"$ENV_FILE" + log "Added ${key}" + fi +} + log "Checking environment variables..." +set_env_var "REGISTRY_URL" "$REGISTRY_URL" update_env_var "PUSHER_APP_ID" "$(openssl rand -hex 32)" update_env_var "PUSHER_APP_KEY" "$(openssl rand -hex 32)" update_env_var "PUSHER_APP_SECRET" "$(openssl rand -hex 32)" @@ -173,7 +193,7 @@ echo "3/6 Pulling Docker images..." echo " This may take a few minutes depending on your connection." # Also pull the helper image (not in compose files but needed for upgrade) -HELPER_IMAGE="${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}" +HELPER_IMAGE="${REGISTRY_URL:-docker.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}" echo " - Pulling $HELPER_IMAGE..." log "Pulling image: $HELPER_IMAGE" if docker pull "$HELPER_IMAGE" >>"$LOGFILE" 2>&1; then @@ -265,7 +285,7 @@ nohup bash -c " fi log 'Running docker compose up...' - docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env \${COMPOSE_FILES} up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1 + docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-docker.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env \${COMPOSE_FILES} up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1 log 'Docker compose up completed' # Final log entry diff --git a/tests/Feature/SettingsUpdatesAuthorizationTest.php b/tests/Feature/SettingsUpdatesAuthorizationTest.php index 5a062101a..353606c60 100644 --- a/tests/Feature/SettingsUpdatesAuthorizationTest.php +++ b/tests/Feature/SettingsUpdatesAuthorizationTest.php @@ -26,7 +26,7 @@ test('instance admin can access settings updates page', function () { $rootTeam = Team::find(0) ?? Team::factory()->create(['id' => 0]); Server::factory()->create(['id' => 0, 'team_id' => $rootTeam->id]); - InstanceSettings::create(['id' => 0]); + InstanceSettings::forceCreate(['id' => 0]); Once::flush(); $user = User::factory()->create(); @@ -39,3 +39,24 @@ ->assertOk() ->assertNoRedirect(); }); + +test('instance admin cannot save an invalid docker registry url', function () { + config()->set('constants.coolify.self_hosted', false); + + $rootTeam = Team::find(0) ?? Team::factory()->create(['id' => 0]); + $settings = InstanceSettings::forceCreate(['id' => 0]); + Once::flush(); + + $user = User::factory()->create(); + $rootTeam->members()->attach($user->id, ['role' => 'admin']); + + $this->actingAs($user); + session(['currentTeam' => ['id' => $rootTeam->id]]); + + Livewire::test(Updates::class) + ->set('docker_registry_url', 'docker.io; touch /tmp/pwned') + ->call('instantSave') + ->assertHasErrors(['docker_registry_url' => 'in']); + + expect($settings->fresh()->docker_registry_url)->toBe('docker.io'); +}); diff --git a/tests/Unit/UpdateCoolifyTest.php b/tests/Unit/UpdateCoolifyTest.php index b3f496d68..833fe3603 100644 --- a/tests/Unit/UpdateCoolifyTest.php +++ b/tests/Unit/UpdateCoolifyTest.php @@ -1,21 +1,43 @@ mockServer = Mockery::mock(Server::class)->makePartial(); - $this->mockServer->id = 0; +uses(TestCase::class, RefreshDatabase::class); - // Mock InstanceSettings - $this->settings = Mockery::mock(InstanceSettings::class); - $this->settings->is_auto_update_enabled = true; - $this->settings->shouldReceive('save')->andReturn(true); -}); +function updateCoolifyTestCreateRootServerAndSettings(array $settings = []): void +{ + Team::factory()->create(['id' => 0]); + Server::forceCreate([ + 'id' => 0, + 'name' => 'localhost', + 'ip' => '127.0.0.1', + 'user' => 'root', + 'team_id' => 0, + 'private_key_id' => 1, + ]); + InstanceSettings::forceCreate(array_merge([ + 'id' => 0, + 'is_auto_update_enabled' => true, + 'auto_update_frequency' => '0 0 * * *', + 'update_check_frequency' => '0 * * * *', + ], $settings)); + Once::flush(); +} afterEach(function () { Mockery::close(); @@ -26,15 +48,7 @@ }); it('validates cache against running version before fallback', function () { - // Mock Server::find to return our mock server - Server::shouldReceive('find') - ->with(0) - ->andReturn($this->mockServer); - - // Mock instanceSettings - $this->app->instance('App\Models\InstanceSettings', function () { - return $this->settings; - }); + updateCoolifyTestCreateRootServerAndSettings(); // CDN fails Http::fake(['*' => Http::response(null, 500)]); @@ -51,7 +65,7 @@ try { $action->handle(manual_update: false); expect(false)->toBeTrue('Expected exception was not thrown'); - } catch (\Exception $e) { + } catch (Exception $e) { expect($e->getMessage())->toContain('cache version'); expect($e->getMessage())->toContain('4.0.5'); expect($e->getMessage())->toContain('4.0.10'); @@ -59,15 +73,9 @@ }); it('uses validated cache when CDN fails and cache is newer', function () { - // Mock Server::find - Server::shouldReceive('find') - ->with(0) - ->andReturn($this->mockServer); - - // Mock instanceSettings - $this->app->instance('App\Models\InstanceSettings', function () { - return $this->settings; - }); + updateCoolifyTestCreateRootServerAndSettings(); + Queue::fake(); + config(['constants.ssh.mux_enabled' => false]); // CDN fails Http::fake(['*' => Http::response(null, 500)]); @@ -78,12 +86,9 @@ config(['constants.coolify.version' => '4.0.5']); - // Mock the update method to prevent actual update - $action = Mockery::mock(UpdateCoolify::class)->makePartial(); - $action->shouldReceive('update')->once(); - $action->server = $this->mockServer; + $action = new UpdateCoolify; - \Illuminate\Support\Facades\Log::shouldReceive('warning') + Log::shouldReceive('warning') ->once() ->with('Failed to fetch fresh version from CDN, using validated cache', Mockery::type('array')); @@ -93,16 +98,198 @@ expect($action->latestVersion)->toBe('4.0.10'); }); -it('prevents downgrade even with manual update', function () { - // Mock Server::find - Server::shouldReceive('find') - ->with(0) - ->andReturn($this->mockServer); +it('passes the saved registry URL to the upgrade script command', function () { + Queue::fake(); + config([ + 'app.env' => 'testing', + 'constants.coolify.version' => '4.0.9', + 'constants.coolify.helper_version' => '1.0.14', + 'constants.coolify.upgrade_script_url' => 'https://cdn.example.com/upgrade.sh', + 'constants.ssh.mux_enabled' => false, + ]); - // Mock instanceSettings - $this->app->instance('App\Models\InstanceSettings', function () { - return $this->settings; - }); + updateCoolifyTestCreateRootServerAndSettings([ + 'is_auto_update_enabled' => true, + 'docker_registry_url' => 'ghcr.io', + ]); + + Http::fake([ + '*' => Http::response([ + 'coolify' => ['v4' => ['version' => '4.0.10']], + ], 200), + ]); + + (new UpdateCoolify)->handle(); + + expect(Activity::query()->latest('id')->first()?->getExtraProperty('command'))->toBe( + "curl -fsSL https://cdn.example.com/upgrade.sh -o /data/coolify/source/upgrade.sh\n". + "bash /data/coolify/source/upgrade.sh '4.0.10' '1.0.14' 'ghcr.io'" + ); +}); + +it('falls back to docker io for the upgrade script command when no registry is saved', function () { + Queue::fake(); + config([ + 'app.env' => 'testing', + 'constants.coolify.version' => '4.0.9', + 'constants.coolify.helper_version' => '1.0.14', + 'constants.coolify.registry_url' => 'ghcr.io', + 'constants.coolify.upgrade_script_url' => 'https://cdn.example.com/upgrade.sh', + 'constants.ssh.mux_enabled' => false, + ]); + + updateCoolifyTestCreateRootServerAndSettings([ + 'is_auto_update_enabled' => true, + ]); + + Http::fake([ + '*' => Http::response([ + 'coolify' => ['v4' => ['version' => '4.0.10']], + ], 200), + ]); + + (new UpdateCoolify)->handle(); + + expect(Activity::query()->latest('id')->first()?->getExtraProperty('command'))->toBe( + "curl -fsSL https://cdn.example.com/upgrade.sh -o /data/coolify/source/upgrade.sh\n". + "bash /data/coolify/source/upgrade.sh '4.0.10' '1.0.14' 'docker.io'" + ); +}); + +it('defaults the registry setting to docker io when no registry is saved', function () { + config([ + 'app.env' => 'testing', + 'constants.coolify.registry_url' => 'ghcr.io', + 'constants.coolify.self_hosted' => true, + ]); + + updateCoolifyTestCreateRootServerAndSettings(); + + $rootTeam = Team::findOrFail(0); + $user = User::factory()->create(); + $rootTeam->members()->attach($user->id, ['role' => 'admin']); + + $this->actingAs($user); + session(['currentTeam' => ['id' => $rootTeam->id]]); + + Livewire::test(Updates::class) + ->assertSet('docker_registry_url', 'docker.io'); +}); + +it('uses the database registry for helper images when the configured helper image is default', function () { + config([ + 'constants.coolify.registry_url' => 'ghcr.io', + 'constants.coolify.helper_image' => 'ghcr.io/coollabsio/coolify-helper', + ]); + + updateCoolifyTestCreateRootServerAndSettings([ + 'docker_registry_url' => 'docker.io', + ]); + + expect(coolifyRegistryUrl())->toBe('docker.io') + ->and(coolifyHelperImage())->toBe('docker.io/coollabsio/coolify-helper'); +}); + +it('preserves an explicit custom helper image override', function () { + config([ + 'constants.coolify.registry_url' => 'docker.io', + 'constants.coolify.helper_image' => 'registry.example.com/custom/helper', + ]); + + updateCoolifyTestCreateRootServerAndSettings([ + 'docker_registry_url' => 'ghcr.io', + ]); + + expect(coolifyHelperImage())->toBe('registry.example.com/custom/helper'); +}); + +it('uses the database registry for sentinel images', function () { + $action = file_get_contents(app_path('Actions/Server/StartSentinel.php')); + + expect($action)->toContain("\$image = coolifyRegistryUrl().'/coollabsio/sentinel:'.\$version;"); +}); + +it('rejects invalid registry values and does not sync them', function () { + Process::fake(); + config([ + 'app.env' => 'testing', + 'constants.coolify.registry_url' => 'docker.io', + ]); + + updateCoolifyTestCreateRootServerAndSettings([ + 'is_auto_update_enabled' => true, + 'auto_update_frequency' => '0 0 * * *', + 'update_check_frequency' => '0 * * * *', + 'docker_registry_url' => 'docker.io', + ]); + + $rootTeam = Team::findOrFail(0); + $user = User::factory()->create(); + $rootTeam->members()->attach($user->id, ['role' => 'admin']); + + $this->actingAs($user); + session(['currentTeam' => ['id' => $rootTeam->id]]); + + Livewire::test(Updates::class) + ->set('docker_registry_url', 'ghcr.io; touch /tmp/pwned') + ->call('submit') + ->assertHasErrors(['docker_registry_url' => ['in']]); + + expect(InstanceSettings::findOrFail(0)->docker_registry_url)->toBe('docker.io'); + Process::assertDidntRun(fn () => true); +}); + +it('does not save registry changes when syncing the env file fails', function () { + config([ + 'app.env' => 'testing', + 'constants.coolify.registry_url' => 'docker.io', + 'constants.coolify.self_hosted' => true, + ]); + + updateCoolifyTestCreateRootServerAndSettings([ + 'is_auto_update_enabled' => true, + 'auto_update_frequency' => '0 0 * * *', + 'update_check_frequency' => '0 * * * *', + 'docker_registry_url' => 'docker.io', + ]); + + $rootTeam = Team::findOrFail(0); + $user = User::factory()->create(); + $rootTeam->members()->attach($user->id, ['role' => 'admin']); + + $this->actingAs($user); + session(['currentTeam' => ['id' => $rootTeam->id]]); + + $component = new class extends Updates + { + protected function syncRegistryUrlToEnv(string $registryUrl): void + { + throw new RuntimeException('sync failed'); + } + }; + $component->settings = InstanceSettings::findOrFail(0); + $component->auto_update_frequency = '0 0 * * *'; + $component->update_check_frequency = '0 * * * *'; + $component->is_auto_update_enabled = true; + $component->docker_registry_url = 'ghcr.io'; + + $component->instantSave(); + + expect(InstanceSettings::findOrFail(0)->docker_registry_url)->toBe('docker.io'); +}); + +it('appends registry url to env file when the key is missing', function () { + $component = new Updates; + $method = new ReflectionMethod(Updates::class, 'registryEnvSyncCommand'); + + expect($method->invoke($component, 'ghcr.io')) + ->toContain("grep -q '^REGISTRY_URL=' /data/coolify/source/.env") + ->toContain("sed -i 's|^REGISTRY_URL=.*|REGISTRY_URL=ghcr.io|' /data/coolify/source/.env") + ->toContain("printf '%s\\n' 'REGISTRY_URL=ghcr.io' >> /data/coolify/source/.env"); +}); + +it('prevents downgrade even with manual update', function () { + updateCoolifyTestCreateRootServerAndSettings(); // CDN returns older version Http::fake([ @@ -116,7 +303,7 @@ $action = new UpdateCoolify; - \Illuminate\Support\Facades\Log::shouldReceive('error') + Log::shouldReceive('error') ->once() ->with('Downgrade prevented', Mockery::type('array')); @@ -124,7 +311,7 @@ try { $action->handle(manual_update: true); expect(false)->toBeTrue('Expected exception was not thrown'); - } catch (\Exception $e) { + } catch (Exception $e) { expect($e->getMessage())->toContain('Cannot downgrade'); expect($e->getMessage())->toContain('4.0.10'); expect($e->getMessage())->toContain('4.0.0'); diff --git a/tests/Unit/UpgradePostgresScriptTest.php b/tests/Unit/UpgradePostgresScriptTest.php index 49a6b881f..a888bc3a4 100644 --- a/tests/Unit/UpgradePostgresScriptTest.php +++ b/tests/Unit/UpgradePostgresScriptTest.php @@ -44,6 +44,38 @@ function assertBashSyntaxIsValid(string $path): void 'nightly upgrade' => 'other/nightly/upgrade.sh', ]); +it('uses the selected registry url when extracting upgrade images', function (string $path) { + $script = file_get_contents(getcwd().'/'.$path); + + expect($script)->toContain('IMAGES=$(REGISTRY_URL=${REGISTRY_URL} LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file "$ENV_FILE" $COMPOSE_FILES config --images'); +})->with([ + 'stable upgrade' => 'scripts/upgrade.sh', + 'nightly upgrade' => 'other/nightly/upgrade.sh', +]); + +it('persists the selected registry url during upgrades', function (string $path) { + $script = file_get_contents(getcwd().'/'.$path); + + expect($script)->toContain('set_env_var "REGISTRY_URL" "$REGISTRY_URL"'); +})->with([ + 'stable upgrade' => 'scripts/upgrade.sh', + 'nightly upgrade' => 'other/nightly/upgrade.sh', +]); + +it('uses the existing env registry url when old callers do not pass a registry argument', function (string $path) { + $script = file_get_contents(getcwd().'/'.$path); + + expect($script) + ->toContain('if [ -n "${3+x}" ]; then') + ->toContain('REGISTRY_URL="$3"') + ->toContain('elif [ -f "$ENV_FILE" ] && grep -q "^REGISTRY_URL=" "$ENV_FILE"; then') + ->toContain("REGISTRY_URL=$(grep \"^REGISTRY_URL=\" \"\$ENV_FILE\" | cut -d '=' -f2- | head -n1)") + ->toContain('REGISTRY_URL="docker.io"'); +})->with([ + 'stable upgrade' => 'scripts/upgrade.sh', + 'nightly upgrade' => 'other/nightly/upgrade.sh', +]); + it('keeps postgres upgrade compose override in future upgrade compose commands', function (string $path) { $script = file_get_contents(getcwd().'/'.$path);