fix(installer): keep resource data ownership on reinstall and upgrade

install.sh ran chown -R 9999:root and chmod -R 700 on all of
/data/coolify on every run. This changed bind-mounted data of services,
applications, and databases, and reset the ownership that a non-root
SSH user needs.

Set owner and mode on /data/coolify and the resource directories only
when the installer creates them. Change only source, ssh, and images
recursively, because the coolify container uses them.

Also wait until the Docker daemon accepts connections after a restart,
because OpenRC returns before the daemon is ready.

Fixes #11945
This commit is contained in:
Andras Bacsai
2026-09-29 10:37:54 +02:00
parent 3452faec87
commit 65262e639e
3 changed files with 156 additions and 20 deletions
+18 -7
View File
@@ -29,12 +29,24 @@ if [ $EUID != 0 ]; then
exit 1
fi
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,sentinel}
mkdir -p /data/coolify/ssh/{keys,mux}
mkdir -p /data/coolify/proxy/dynamic
# Resource directories hold data whose owner and mode the resource images or a non-root SSH user need.
# Set their owner and mode only when they are created, never on an upgrade (#11945).
for COOLIFY_DIRECTORY in /data/coolify /data/coolify/{applications,databases,backups,services,proxy,proxy/dynamic,sentinel}; do
if [ ! -d "$COOLIFY_DIRECTORY" ]; then
mkdir -p "$COOLIFY_DIRECTORY"
chown 9999:root "$COOLIFY_DIRECTORY"
chmod 700 "$COOLIFY_DIRECTORY"
fi
done
mkdir -p /data/coolify/source /data/coolify/ssh/{keys,mux}
chown -R 9999:root /data/coolify
chmod -R 700 /data/coolify
# Coolify's own directories, used by the coolify container (UID 9999)
set_coolify_directory_permissions() {
chown -R 9999:root /data/coolify/{source,ssh}
chmod -R 700 /data/coolify/{source,ssh}
}
set_coolify_directory_permissions
INSTALLATION_LOG_WITH_DATE="/data/coolify/source/installation-${DATE}.log"
@@ -1071,8 +1083,7 @@ if [ "$IS_COOLIFY_VOLUME_EXISTS" -eq 0 ]; then
rm -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal.pub
fi
chown -R 9999:root /data/coolify
chmod -R 700 /data/coolify
set_coolify_directory_permissions
log "SSH key check completed"
step_done "$SSH_KEY_DETAIL"
+33 -13
View File
@@ -29,13 +29,24 @@ if [ $EUID != 0 ]; then
exit 1
fi
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,sentinel}
mkdir -p /data/coolify/images
mkdir -p /data/coolify/ssh/{keys,mux}
mkdir -p /data/coolify/proxy/dynamic
# Resource directories hold data whose owner and mode the resource images or a non-root SSH user need.
# Set their owner and mode only when they are created, never on an upgrade (#11945).
for COOLIFY_DIRECTORY in /data/coolify /data/coolify/{applications,databases,backups,services,proxy,proxy/dynamic,sentinel}; do
if [ ! -d "$COOLIFY_DIRECTORY" ]; then
mkdir -p "$COOLIFY_DIRECTORY"
chown 9999:root "$COOLIFY_DIRECTORY"
chmod 700 "$COOLIFY_DIRECTORY"
fi
done
mkdir -p /data/coolify/source /data/coolify/images /data/coolify/ssh/{keys,mux}
chown -R 9999:root /data/coolify
chmod -R 700 /data/coolify
# Coolify's own directories, used by the coolify container (UID 9999)
set_coolify_directory_permissions() {
chown -R 9999:root /data/coolify/{source,ssh,images}
chmod -R 700 /data/coolify/{source,ssh,images}
}
set_coolify_directory_permissions
INSTALLATION_LOG_WITH_DATE="/data/coolify/source/installation-${DATE}.log"
@@ -278,11 +289,21 @@ if [ -n "${DOCKER_ADDRESS_POOL_SIZE+x}" ]; then
DOCKER_POOL_SIZE_PROVIDED=true
fi
# OpenRC (Alpine) returns from "service docker start" before the daemon accepts connections
wait_for_docker_daemon() {
for _ in $(seq 1 30); do
if docker info >/dev/null 2>&1; then
return 0
fi
sleep 1
done
return 1
}
restart_docker_service() {
# Check if systemctl is available
if command -v systemctl >/dev/null 2>&1; then
systemctl restart docker
if [ $? -eq 0 ]; then
if systemctl restart docker && wait_for_docker_daemon; then
echo " - Docker daemon restarted successfully"
else
echo " - Failed to restart Docker daemon"
@@ -290,8 +311,7 @@ restart_docker_service() {
fi
# Check if service command is available
elif command -v service >/dev/null 2>&1; then
service docker restart
if [ $? -eq 0 ]; then
if service docker restart && wait_for_docker_daemon; then
echo " - Docker daemon restarted successfully"
else
echo " - Failed to restart Docker daemon"
@@ -792,7 +812,8 @@ fi
# Verify minimum Docker version
MIN_DOCKER_VERSION=24
INSTALLED_DOCKER_VERSION=$(docker version --format '{{.Server.Version}}' 2>/dev/null | cut -d. -f1)
wait_for_docker_daemon || true
INSTALLED_DOCKER_VERSION=$(docker version --format '{{.Server.Version}}' 2>/dev/null | cut -d. -f1 || true)
if [ -z "$INSTALLED_DOCKER_VERSION" ]; then
warn "Could not determine Docker version. Please ensure Docker $MIN_DOCKER_VERSION+ is installed."
elif [ "$INSTALLED_DOCKER_VERSION" -lt "$MIN_DOCKER_VERSION" ]; then
@@ -1072,8 +1093,7 @@ if [ "$IS_COOLIFY_VOLUME_EXISTS" -eq 0 ]; then
rm -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal.pub
fi
chown -R 9999:root /data/coolify
chmod -R 700 /data/coolify
set_coolify_directory_permissions
log "SSH key check completed"
step_done "$SSH_KEY_DETAIL"
@@ -0,0 +1,105 @@
<?php
/**
* Runs the /data/coolify directory setup of the install script against a temporary data root.
* chown is replaced with a logger because changing the owner to UID 9999 needs root.
*
* @return list<string> the paths that chown received
*/
function runInstallScriptDirectorySetup(string $scriptPath, string $dataRoot): array
{
$script = file_get_contents(dirname(__DIR__, 2).'/'.$scriptPath);
preg_match('/^# Resource directories.*?^set_coolify_directory_permissions$/ms', $script, $matches);
expect($matches)->not->toBeEmpty();
$setup = str_replace('/data/coolify', $dataRoot, $matches[0]);
$chownLog = "{$dataRoot}.chown";
$process = proc_open(
['bash', '-c', "set -e\nchown() { printf '%s\\n' \"\$@\" >> ".escapeshellarg($chownLog)."; }\n{$setup}"],
[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);
$chownedPaths = array_filter(file($chownLog, FILE_IGNORE_NEW_LINES), fn (string $argument) => str_starts_with($argument, '/'));
unlink($chownLog);
return array_values(array_map(fn (string $path) => str_replace($dataRoot, '/data/coolify', $path), $chownedPaths));
}
function installScriptFileMode(string $path): string
{
clearstatcache(true, $path);
return substr(sprintf('%o', fileperms($path)), -4);
}
$installScripts = [
'stable install' => ['scripts/install.sh', ['source', 'ssh', 'images']],
'nightly install' => ['other/nightly/install.sh', ['source', 'ssh']],
];
$resourceDirectories = ['applications', 'databases', 'backups', 'services', 'proxy', 'sentinel'];
it('creates the data directories as uid 9999 with mode 700 on a fresh install', function (string $scriptPath, array $coolifyDirectories) use ($resourceDirectories) {
$dataRoot = sys_get_temp_dir().'/coolify-install-permissions-'.bin2hex(random_bytes(6));
try {
$chownedPaths = runInstallScriptDirectorySetup($scriptPath, $dataRoot);
foreach (['', ...array_map(fn ($directory) => "/{$directory}", $resourceDirectories), '/proxy/dynamic'] as $directory) {
expect($chownedPaths)->toContain("/data/coolify{$directory}")
->and(installScriptFileMode("{$dataRoot}{$directory}"))->toBe('0700');
}
foreach ($coolifyDirectories as $directory) {
expect($chownedPaths)->toContain("/data/coolify/{$directory}")
->and(installScriptFileMode("{$dataRoot}/{$directory}"))->toBe('0700');
}
} finally {
exec('rm -rf '.escapeshellarg($dataRoot));
}
})->with($installScripts);
it('keeps existing resource directories and their content unchanged on an upgrade', function (string $scriptPath, array $coolifyDirectories) use ($resourceDirectories) {
$dataRoot = sys_get_temp_dir().'/coolify-install-permissions-'.bin2hex(random_bytes(6));
try {
// A non-root server user setup: the SSH user owns the tree with mode 750.
mkdir($dataRoot, 0750);
chmod($dataRoot, 0750);
foreach ([...$resourceDirectories, 'proxy/dynamic', ...$coolifyDirectories] as $directory) {
mkdir("{$dataRoot}/{$directory}/nested", 0755, true);
chmod("{$dataRoot}/{$directory}", 0750);
touch("{$dataRoot}/{$directory}/nested/file");
chmod("{$dataRoot}/{$directory}/nested/file", 0644);
}
$chownedPaths = runInstallScriptDirectorySetup($scriptPath, $dataRoot);
expect($chownedPaths)->toBe(array_map(fn ($directory) => "/data/coolify/{$directory}", $coolifyDirectories))
->and(installScriptFileMode($dataRoot))->toBe('0750');
foreach ([...$resourceDirectories, 'proxy/dynamic'] as $directory) {
expect(installScriptFileMode("{$dataRoot}/{$directory}"))->toBe('0750')
->and(installScriptFileMode("{$dataRoot}/{$directory}/nested"))->toBe('0755')
->and(installScriptFileMode("{$dataRoot}/{$directory}/nested/file"))->toBe('0644');
}
foreach ($coolifyDirectories as $directory) {
expect(installScriptFileMode("{$dataRoot}/{$directory}/nested"))->toBe('0700')
->and(installScriptFileMode("{$dataRoot}/{$directory}/nested/file"))->toBe('0700');
}
} finally {
exec('rm -rf '.escapeshellarg($dataRoot));
}
})->with($installScripts);
it('does not change the whole data directory recursively', function (string $scriptPath) {
$script = file_get_contents(dirname(__DIR__, 2).'/'.$scriptPath);
expect($script)
->not->toContain("chown -R 9999:root /data/coolify\n")
->not->toContain("chmod -R 700 /data/coolify\n")
->and(substr_count($script, "\nset_coolify_directory_permissions\n"))->toBe(2);
})->with(array_map(fn (array $script) => $script[0], $installScripts));