From 43933ee6ccb55b1e700673882e2d3da60f4fcd46 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 29 Oct 2026 18:25:50 +0100 Subject: [PATCH] fix(upgrade): quote root user credentials with spaces in the .env file Installers before 4.5.1 and manual edits could leave ROOT_USERNAME (or the root email or password) with spaces and no quotes. The running container ignores .env because its config is cached, so the next upgrade recreated it and phpdotenv failed, which made every page return HTTP 500. The upgrade now quotes these values before it restarts Coolify. Closes #12375 Co-Authored-By: Claude Opus 5.5 --- other/nightly/upgrade.sh | 35 +++++++++++ scripts/upgrade.sh | 35 +++++++++++ tests/Unit/UpgradeScriptRootUserEnvTest.php | 67 +++++++++++++++++++++ 3 files changed, 137 insertions(+) create mode 100644 tests/Unit/UpgradeScriptRootUserEnvTest.php diff --git a/other/nightly/upgrade.sh b/other/nightly/upgrade.sh index 4d594cb9e9..35c087db69 100644 --- a/other/nightly/upgrade.sh +++ b/other/nightly/upgrade.sh @@ -205,7 +205,42 @@ set_env_var() { fi } +# Quote a value so that Docker Compose and phpdotenv both read it back unchanged (same as install.sh). +quote_env_value() { + local value="$1" + case "$value" in + *\'*) + value=${value//\\/\\\\} + value=${value//\"/\\\"} + value=${value//\$/\\\$} + printf '"%s"' "$value" + ;; + *) + printf "'%s'" "$value" + ;; + esac +} + +# Installers before 4.5.1 (and manual edits) wrote root user credentials with spaces without quotes. +# phpdotenv cannot parse such a line, so the recreated Coolify container returns HTTP 500 for every page. +# Only quote lines that phpdotenv rejects: unquoted values that contain whitespace but no comment. +quote_root_user_env_values() { + local key value + for key in ROOT_USERNAME ROOT_USER_EMAIL ROOT_USER_PASSWORD; do + value=$(grep -m1 "^${key}=" "$ENV_FILE" | cut -d '=' -f2-) || continue + case "$value" in + \"* | \'* | *[[:space:]]\#*) continue ;; + *[[:space:]]*) ;; + *) continue ;; + esac + # awk reads the value from the environment, so characters such as | & \ in it stay literal + KEY="$key" VALUE="$(quote_env_value "$value")" awk '!done && index($0, ENVIRON["KEY"] "=") == 1 { $0 = ENVIRON["KEY"] "=" ENVIRON["VALUE"]; done = 1 } { print }' "$ENV_FILE" >"$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE" + log "Quoted ${key} (its value has spaces)" + done +} + log "Checking environment variables..." +quote_root_user_env_values 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)" diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index 1e1e23cadd..9377d79c5a 100644 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh @@ -205,7 +205,42 @@ set_env_var() { fi } +# Quote a value so that Docker Compose and phpdotenv both read it back unchanged (same as install.sh). +quote_env_value() { + local value="$1" + case "$value" in + *\'*) + value=${value//\\/\\\\} + value=${value//\"/\\\"} + value=${value//\$/\\\$} + printf '"%s"' "$value" + ;; + *) + printf "'%s'" "$value" + ;; + esac +} + +# Installers before 4.5.1 (and manual edits) wrote root user credentials with spaces without quotes. +# phpdotenv cannot parse such a line, so the recreated Coolify container returns HTTP 500 for every page. +# Only quote lines that phpdotenv rejects: unquoted values that contain whitespace but no comment. +quote_root_user_env_values() { + local key value + for key in ROOT_USERNAME ROOT_USER_EMAIL ROOT_USER_PASSWORD; do + value=$(grep -m1 "^${key}=" "$ENV_FILE" | cut -d '=' -f2-) || continue + case "$value" in + \"* | \'* | *[[:space:]]\#*) continue ;; + *[[:space:]]*) ;; + *) continue ;; + esac + # awk reads the value from the environment, so characters such as | & \ in it stay literal + KEY="$key" VALUE="$(quote_env_value "$value")" awk '!done && index($0, ENVIRON["KEY"] "=") == 1 { $0 = ENVIRON["KEY"] "=" ENVIRON["VALUE"]; done = 1 } { print }' "$ENV_FILE" >"$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE" + log "Quoted ${key} (its value has spaces)" + done +} + log "Checking environment variables..." +quote_root_user_env_values 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)" diff --git a/tests/Unit/UpgradeScriptRootUserEnvTest.php b/tests/Unit/UpgradeScriptRootUserEnvTest.php new file mode 100644 index 0000000000..e77ea3f6a2 --- /dev/null +++ b/tests/Unit/UpgradeScriptRootUserEnvTest.php @@ -0,0 +1,67 @@ +not->toBeEmpty() + ->and($quoteRootUserEnvValues)->not->toBeEmpty(); + + $envFile = tempnam(sys_get_temp_dir(), 'coolify-upgrade-env-'); + file_put_contents($envFile, $initialEnv); + + $process = proc_open( + ['bash', '-c', implode("\n", ['set -e', 'log() { :; }', 'ENV_FILE='.escapeshellarg($envFile), $quoteEnvValue[0], $quoteRootUserEnvValues[0], 'quote_root_user_env_values'])], + [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], + $pipes + ); + $output = stream_get_contents($pipes[1]).stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + expect(proc_close($process))->toBe(0, $output); + + $contents = file_get_contents($envFile); + unlink($envFile); + + return $contents; +} + +$upgradeScripts = ['stable upgrade' => 'scripts/upgrade.sh', 'nightly upgrade' => 'other/nightly/upgrade.sh']; + +it('quotes unquoted root user credentials with spaces so that .env parses again', function (string $scriptPath) { + $env = repairRootUserEnvWithUpgradeScript($scriptPath, implode("\n", [ + 'APP_NAME=Coolify', + 'ROOT_USERNAME=Jane Doe', + 'ROOT_USER_EMAIL=admin@example.com', + "ROOT_USER_PASSWORD=it's a P@ss|&\\ \$HOME", + 'APP_ENV=production', + '', + ])); + + expect(Dotenv::parse($env))->toBe([ + 'APP_NAME' => 'Coolify', + 'ROOT_USERNAME' => 'Jane Doe', + 'ROOT_USER_EMAIL' => 'admin@example.com', + 'ROOT_USER_PASSWORD' => "it's a P@ss|&\\ \$HOME", + 'APP_ENV' => 'production', + ]); +})->with($upgradeScripts); + +it('keeps root user credentials that already parse unchanged', function (string $scriptPath) { + $initialEnv = implode("\n", [ + 'ROOT_USERNAME="Jane Doe"', + "ROOT_USER_EMAIL='admin@example.com'", + 'ROOT_USER_PASSWORD=', + 'APP_NAME=My Coolify', + '', + ]); + + expect(repairRootUserEnvWithUpgradeScript($scriptPath, $initialEnv))->toBe($initialEnv); +})->with($upgradeScripts);