fix(install): quote root user credentials in the .env file

ROOT_USERNAME with a space (or a password with #, $ or quotes) was written
unquoted, so phpdotenv failed to parse .env and Coolify returned HTTP 500.

Closes #12276

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-10-23 22:20:31 +02:00
co-authored by Claude Opus 5.5
parent 54c930d8dd
commit d37b7356e7
3 changed files with 113 additions and 8 deletions
+22 -4
View File
@@ -929,7 +929,8 @@ update_env_var() {
# If variable "key=" exists but has no value, update the value of the existing line
if grep -q "^${key}=$" "$ENV_FILE"; then
sed -i "s|^${key}=$|${key}=${value}|" "$ENV_FILE"
# awk reads the value from the environment, so characters such as | & \ in it stay literal
KEY="$key" VALUE="$value" awk '$0 == ENVIRON["KEY"] "=" { $0 = ENVIRON["KEY"] "=" ENVIRON["VALUE"] } { print }' "$ENV_FILE" >"$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE"
echo " - Updated value of ${key} as the current value was empty"
# If variable "key=" doesn't exist, append it to the file with value
elif ! grep -q "^${key}=" "$ENV_FILE"; then
@@ -938,6 +939,23 @@ update_env_var() {
fi
}
# Quote a user-supplied value so that Docker Compose and phpdotenv both read it back unchanged
# (spaces, #, $ and quotes otherwise break parsing or change the value).
quote_env_value() {
local value="$1"
case "$value" in
*\'*)
value=${value//\\/\\\\}
value=${value//\"/\\\"}
value=${value//\$/\\\$}
printf '"%s"' "$value"
;;
*)
printf "'%s'" "$value"
;;
esac
}
update_env_var "APP_ID" "$(openssl rand -hex 16)"
update_env_var "APP_KEY" "base64:$(openssl rand -base64 32)"
# update_env_var "DB_USERNAME" "$(openssl rand -hex 16)" # Causes issues: database "random-user" does not exist
@@ -951,9 +969,9 @@ update_env_var "PUSHER_BACKEND_PORT" "6001"
# Add default root user credentials from environment variables
if [ -n "$ROOT_USERNAME" ] && [ -n "$ROOT_USER_EMAIL" ] && [ -n "$ROOT_USER_PASSWORD" ]; then
echo " - Setting predefined root user credentials from environment"
update_env_var "ROOT_USERNAME" "$ROOT_USERNAME"
update_env_var "ROOT_USER_EMAIL" "$ROOT_USER_EMAIL"
update_env_var "ROOT_USER_PASSWORD" "$ROOT_USER_PASSWORD"
update_env_var "ROOT_USERNAME" "$(quote_env_value "$ROOT_USERNAME")"
update_env_var "ROOT_USER_EMAIL" "$(quote_env_value "$ROOT_USER_EMAIL")"
update_env_var "ROOT_USER_PASSWORD" "$(quote_env_value "$ROOT_USER_PASSWORD")"
fi
if [ -n "${REGISTRY_URL+x}" ]; then
+22 -4
View File
@@ -929,7 +929,8 @@ update_env_var() {
# If variable "key=" exists but has no value, update the value of the existing line
if grep -q "^${key}=$" "$ENV_FILE"; then
sed -i "s|^${key}=$|${key}=${value}|" "$ENV_FILE"
# awk reads the value from the environment, so characters such as | & \ in it stay literal
KEY="$key" VALUE="$value" awk '$0 == ENVIRON["KEY"] "=" { $0 = ENVIRON["KEY"] "=" ENVIRON["VALUE"] } { print }' "$ENV_FILE" >"$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE"
echo " - Updated value of ${key} as the current value was empty"
# If variable "key=" doesn't exist, append it to the file with value
elif ! grep -q "^${key}=" "$ENV_FILE"; then
@@ -938,6 +939,23 @@ update_env_var() {
fi
}
# Quote a user-supplied value so that Docker Compose and phpdotenv both read it back unchanged
# (spaces, #, $ and quotes otherwise break parsing or change the value).
quote_env_value() {
local value="$1"
case "$value" in
*\'*)
value=${value//\\/\\\\}
value=${value//\"/\\\"}
value=${value//\$/\\\$}
printf '"%s"' "$value"
;;
*)
printf "'%s'" "$value"
;;
esac
}
update_env_var "APP_ID" "$(openssl rand -hex 16)"
update_env_var "APP_KEY" "base64:$(openssl rand -base64 32)"
# update_env_var "DB_USERNAME" "$(openssl rand -hex 16)" # Causes issues: database "random-user" does not exist
@@ -951,9 +969,9 @@ update_env_var "PUSHER_BACKEND_PORT" "6001"
# Add default root user credentials from environment variables
if [ -n "$ROOT_USERNAME" ] && [ -n "$ROOT_USER_EMAIL" ] && [ -n "$ROOT_USER_PASSWORD" ]; then
echo " - Setting predefined root user credentials from environment"
update_env_var "ROOT_USERNAME" "$ROOT_USERNAME"
update_env_var "ROOT_USER_EMAIL" "$ROOT_USER_EMAIL"
update_env_var "ROOT_USER_PASSWORD" "$ROOT_USER_PASSWORD"
update_env_var "ROOT_USERNAME" "$(quote_env_value "$ROOT_USERNAME")"
update_env_var "ROOT_USER_EMAIL" "$(quote_env_value "$ROOT_USER_EMAIL")"
update_env_var "ROOT_USER_PASSWORD" "$(quote_env_value "$ROOT_USER_PASSWORD")"
fi
if [ -n "${REGISTRY_URL+x}" ]; then
@@ -0,0 +1,69 @@
<?php
use Dotenv\Dotenv;
/**
* Runs the install script's update_env_var and quote_env_value helpers for the root user
* credentials against a temporary .env file, then parses that file the way Laravel does.
*
* @param array<string, string> $credentials
* @return array<string, string|null>
*/
function writeRootUserCredentialsWithInstallScript(string $scriptPath, array $credentials, string $initialEnv): array
{
$script = file_get_contents(dirname(__DIR__, 2).'/'.$scriptPath);
preg_match('/^update_env_var\(\) \{.*?^\}$/ms', $script, $updateEnvVar);
preg_match('/^quote_env_value\(\) \{.*?^\}$/ms', $script, $quoteEnvValue);
expect($updateEnvVar)->not->toBeEmpty()
->and($quoteEnvValue)->not->toBeEmpty();
$envFile = tempnam(sys_get_temp_dir(), 'coolify-install-env-');
file_put_contents($envFile, $initialEnv);
$commands = array_map(
fn (string $key, string $value) => "update_env_var {$key} \"\$(quote_env_value ".escapeshellarg($value).')"',
array_keys($credentials),
$credentials,
);
$process = proc_open(
['bash', '-c', implode("\n", ['set -e', 'ENV_FILE='.escapeshellarg($envFile), $updateEnvVar[0], $quoteEnvValue[0], ...$commands])],
[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 Dotenv::parse($contents);
}
$installScripts = ['stable install' => 'scripts/install.sh', 'nightly install' => 'other/nightly/install.sh'];
$credentials = [
'username with a space' => ['ROOT_USERNAME', 'Coolify Lab'],
'email' => ['ROOT_USER_EMAIL', 'admin@example.com'],
'password with symbols' => ['ROOT_USER_PASSWORD', 'P@ss w0rd#1|&\\$HOME'],
'password with quotes' => ['ROOT_USER_PASSWORD', 'it\'s "Quoted" \\ $HOME #1'],
];
it('writes root user credentials that parse back unchanged into an empty variable', function (string $scriptPath, string $key, string $value) {
$env = writeRootUserCredentialsWithInstallScript($scriptPath, [$key => $value], "APP_NAME=Coolify\n{$key}=\nAPP_ENV=production\n");
expect($env)->toBe(['APP_NAME' => 'Coolify', $key => $value, 'APP_ENV' => 'production']);
})->with($installScripts)->with($credentials);
it('writes root user credentials that parse back unchanged into a missing variable', function (string $scriptPath, string $key, string $value) {
$env = writeRootUserCredentialsWithInstallScript($scriptPath, [$key => $value], "APP_NAME=Coolify\n");
expect($env)->toBe(['APP_NAME' => 'Coolify', $key => $value]);
})->with($installScripts)->with($credentials);
it('keeps root user credentials that are already set', function (string $scriptPath) {
$env = writeRootUserCredentialsWithInstallScript($scriptPath, ['ROOT_USERNAME' => 'New Name'], "ROOT_USERNAME='Old Name'\n");
expect($env)->toBe(['ROOT_USERNAME' => 'Old Name']);
})->with($installScripts);