feat(deployment): add SERVICE_NAME variables for service discovery

This change introduces automatically generated `SERVICE_NAME_<SERVICE>`
environment variables for each service within a Docker Compose deployment.
This allows services to reliably reference each other by name, which is particularly
useful in pull request environments where container names are dynamically suffixed.

- The application parser now generates and injects these `SERVICE_NAME` variables
   into the environment of all services in the compose file.
- `ApplicationDeploymentJob` is updated to correctly handle and filter these
  new variables during deployment.
- UI components and the `EnvironmentVariableProtection` trait have been updated
to make these generated variables read-only, preventing accidental modification.

This commit introduces two new helper functions to standardize resource naming
for pull request deployments:

-  `addPreviewDeploymentSuffix()`: Generates a consistent suffix format (-pr-{id})
   for resource names in preview deployments
-  `generateDockerComposeServiceName()`: Creates SERVICE_NAME environment variables
   for Docker Compose services
This commit is contained in:
Arnaud B
2025-09-08 15:15:57 +02:00
parent 23fb43bb96
commit e23ab1e621
6 changed files with 67 additions and 29 deletions

View File

@@ -221,7 +221,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ($this->pull_request_id === 0) { if ($this->pull_request_id === 0) {
$this->container_name = $this->application->settings->custom_internal_name; $this->container_name = $this->application->settings->custom_internal_name;
} else { } else {
$this->container_name = "{$this->application->settings->custom_internal_name}-pr-{$this->pull_request_id}"; $this->container_name = addPreviewDeploymentSuffix($this->application->settings->custom_internal_name, $this->pull_request_id);
} }
} }
@@ -706,8 +706,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ($this->pull_request_id === 0) { if ($this->pull_request_id === 0) {
$composeFileName = "$mainDir/docker-compose.yaml"; $composeFileName = "$mainDir/docker-compose.yaml";
} else { } else {
$composeFileName = "$mainDir/docker-compose-pr-{$this->pull_request_id}.yaml"; $composeFileName = "$mainDir/".addPreviewDeploymentSuffix('docker-compose', $this->pull_request_id).'.yaml';
$this->docker_compose_location = "/docker-compose-pr-{$this->pull_request_id}.yaml"; $this->docker_compose_location = '/'.addPreviewDeploymentSuffix('docker-compose', $this->pull_request_id).'.yaml';
} }
$this->execute_remote_command([ $this->execute_remote_command([
"mkdir -p $mainDir", "mkdir -p $mainDir",
@@ -898,10 +898,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
} }
if ($this->build_pack === 'dockercompose') { if ($this->build_pack === 'dockercompose') {
$sorted_environment_variables = $sorted_environment_variables->filter(function ($env) { $sorted_environment_variables = $sorted_environment_variables->filter(function ($env) {
return ! str($env->key)->startsWith('SERVICE_FQDN_') && ! str($env->key)->startsWith('SERVICE_URL_'); return ! str($env->key)->startsWith('SERVICE_FQDN_') && ! str($env->key)->startsWith('SERVICE_URL_') && ! str($env->key)->startsWith('SERVICE_NAME_');
}); });
$sorted_environment_variables_preview = $sorted_environment_variables_preview->filter(function ($env) { $sorted_environment_variables_preview = $sorted_environment_variables_preview->filter(function ($env) {
return ! str($env->key)->startsWith('SERVICE_FQDN_') && ! str($env->key)->startsWith('SERVICE_URL_'); return ! str($env->key)->startsWith('SERVICE_FQDN_') && ! str($env->key)->startsWith('SERVICE_URL_') && ! str($env->key)->startsWith('SERVICE_NAME_');
}); });
} }
$ports = $this->application->main_port(); $ports = $this->application->main_port();
@@ -942,9 +942,20 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$envs->push('SERVICE_FQDN_'.str($forServiceName)->upper().'='.$coolifyFqdn); $envs->push('SERVICE_FQDN_'.str($forServiceName)->upper().'='.$coolifyFqdn);
} }
} }
// Generate SERVICE_NAME for dockercompose services from processed compose
if ($this->application->settings->is_raw_compose_deployment_enabled) {
$dockerCompose = Yaml::parse($this->application->docker_compose_raw);
} else {
$dockerCompose = Yaml::parse($this->application->docker_compose);
}
$services = data_get($dockerCompose, 'services', []);
foreach ($services as $serviceName => $_) {
$envs->push('SERVICE_NAME_'.str($serviceName)->upper().'='.$serviceName);
}
} }
} else { } else {
$this->env_filename = ".env-pr-$this->pull_request_id"; $this->env_filename = addPreviewDeploymentSuffix(".env", $this->pull_request_id);
foreach ($sorted_environment_variables_preview as $env) { foreach ($sorted_environment_variables_preview as $env) {
$envs->push($env->key.'='.$env->real_value); $envs->push($env->key.'='.$env->real_value);
} }
@@ -975,6 +986,13 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$envs->push('SERVICE_FQDN_'.str($forServiceName)->upper().'='.$coolifyFqdn); $envs->push('SERVICE_FQDN_'.str($forServiceName)->upper().'='.$coolifyFqdn);
} }
} }
// Generate SERVICE_NAME for dockercompose services
$rawDockerCompose = Yaml::parse($this->application->docker_compose_raw);
$rawServices = data_get($rawDockerCompose, 'services', []);
foreach ($rawServices as $rawServiceName => $_) {
$envs->push('SERVICE_NAME_'.str($rawServiceName)->upper().'='.addPreviewDeploymentSuffix($rawServiceName, $this->pull_request_id));
}
} }
} }
if ($envs->isEmpty()) { if ($envs->isEmpty()) {
@@ -1986,7 +2004,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$volume_name = $persistentStorage->name; $volume_name = $persistentStorage->name;
} }
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
$volume_name = $volume_name.'-pr-'.$this->pull_request_id; $volume_name = addPreviewDeploymentSuffix($volume_name, $this->pull_request_id);
} }
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path; $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
} }
@@ -2004,7 +2022,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$name = $persistentStorage->name; $name = $persistentStorage->name;
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
$name = $name.'-pr-'.$this->pull_request_id; $name = addPreviewDeploymentSuffix($name, $this->pull_request_id);
} }
$local_persistent_volumes_names[$name] = [ $local_persistent_volumes_names[$name] = [
@@ -2301,7 +2319,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id); $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
if ($this->pull_request_id === 0) { if ($this->pull_request_id === 0) {
$containers = $containers->filter(function ($container) { $containers = $containers->filter(function ($container) {
return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== $this->container_name.'-pr-'.$this->pull_request_id; return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== addPreviewDeploymentSuffix($this->container_name, $this->pull_request_id);
}); });
} }
$containers->each(function ($container) { $containers->each(function ($container) {

View File

@@ -257,7 +257,7 @@ class All extends Component
{ {
$count = 0; $count = 0;
foreach ($variables as $key => $value) { foreach ($variables as $key => $value) {
if (str($key)->startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL')) { if (str($key)->startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL') || str($key)->startsWith('SERVICE_NAME')) {
continue; continue;
} }
$method = $isPreview ? 'environment_variables_preview' : 'environment_variables'; $method = $isPreview ? 'environment_variables_preview' : 'environment_variables';

View File

@@ -128,7 +128,7 @@ class Show extends Component
public function checkEnvs() public function checkEnvs()
{ {
$this->isDisabled = false; $this->isDisabled = false;
if (str($this->env->key)->startsWith('SERVICE_FQDN') || str($this->env->key)->startsWith('SERVICE_URL')) { if (str($this->env->key)->startsWith('SERVICE_FQDN') || str($this->env->key)->startsWith('SERVICE_URL') || str($this->env->key)->startsWith('SERVICE_NAME')) {
$this->isDisabled = true; $this->isDisabled = true;
} }
if ($this->env->is_shown_once) { if ($this->env->is_shown_once) {

View File

@@ -14,7 +14,7 @@ trait EnvironmentVariableProtection
*/ */
protected function isProtectedEnvironmentVariable(string $key): bool protected function isProtectedEnvironmentVariable(string $key): bool
{ {
return str($key)->startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL'); return str($key)->startsWith('SERVICE_FQDN_') || str($key)->startsWith('SERVICE_URL_') || str($key)->startsWith('SERVICE_NAME_');
} }
/** /**

View File

@@ -454,6 +454,12 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
} }
} }
// generate SERVICE_NAME variables for docker compose services
$serviceNameEnvironments = collect([]);
if ($resource->build_pack === 'dockercompose') {
$serviceNameEnvironments = generateDockerComposeServiceName($services, $pullRequestId);
}
// Parse the rest of the services // Parse the rest of the services
foreach ($services as $serviceName => $service) { foreach ($services as $serviceName => $service) {
$image = data_get_str($service, 'image'); $image = data_get_str($service, 'image');
@@ -567,7 +573,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
} }
$source = replaceLocalSource($source, $mainDirectory); $source = replaceLocalSource($source, $mainDirectory);
if ($isPullRequest) { if ($isPullRequest) {
$source = $source."-pr-$pullRequestId"; $source = addPreviewDeploymentSuffix($source, $pull_request_id);
} }
LocalFileVolume::updateOrCreate( LocalFileVolume::updateOrCreate(
[ [
@@ -610,7 +616,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
$name = "{$uuid}_{$slugWithoutUuid}"; $name = "{$uuid}_{$slugWithoutUuid}";
if ($isPullRequest) { if ($isPullRequest) {
$name = "{$name}-pr-$pullRequestId"; $name = addPreviewDeploymentSuffix($name, $pull_request_id);
} }
if (is_string($volume)) { if (is_string($volume)) {
$parsed = parseDockerVolumeString($volume); $parsed = parseDockerVolumeString($volume);
@@ -651,11 +657,11 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
$newDependsOn = collect([]); $newDependsOn = collect([]);
$depends_on->each(function ($dependency, $condition) use ($pullRequestId, $newDependsOn) { $depends_on->each(function ($dependency, $condition) use ($pullRequestId, $newDependsOn) {
if (is_numeric($condition)) { if (is_numeric($condition)) {
$dependency = "$dependency-pr-$pullRequestId"; $dependency = addPreviewDeploymentSuffix($dependency, $pullRequestId);
$newDependsOn->put($condition, $dependency); $newDependsOn->put($condition, $dependency);
} else { } else {
$condition = "$condition-pr-$pullRequestId"; $condition = addPreviewDeploymentSuffix($condition, $pullRequestId);
$newDependsOn->put($condition, $dependency); $newDependsOn->put($condition, $dependency);
} }
}); });
@@ -1082,7 +1088,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
$payload['volumes'] = $volumesParsed; $payload['volumes'] = $volumesParsed;
} }
if ($environment->count() > 0 || $coolifyEnvironments->count() > 0) { if ($environment->count() > 0 || $coolifyEnvironments->count() > 0) {
$payload['environment'] = $environment->merge($coolifyEnvironments); $payload['environment'] = $environment->merge($coolifyEnvironments)->merge($serviceNameEnvironments);
} }
if ($logging) { if ($logging) {
$payload['logging'] = $logging; $payload['logging'] = $logging;
@@ -1091,7 +1097,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
$payload['depends_on'] = $depends_on; $payload['depends_on'] = $depends_on;
} }
if ($isPullRequest) { if ($isPullRequest) {
$serviceName = "{$serviceName}-pr-{$pullRequestId}"; $serviceName = addPreviewDeploymentSuffix($serviceName, $pullRequestId);
} }
$parsedServices->put($serviceName, $payload); $parsedServices->put($serviceName, $payload);

View File

@@ -2058,12 +2058,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$name = $name->replaceFirst('~', $dir); $name = $name->replaceFirst('~', $dir);
} }
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$name = $name."-pr-$pull_request_id"; $name = addPreviewDeploymentSuffix($name, $pull_request_id);
} }
$volume = str("$name:$mount"); $volume = str("$name:$mount");
} else { } else {
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$name = $name."-pr-$pull_request_id"; $name = addPreviewDeploymentSuffix($name, $pull_request_id);
$volume = str("$name:$mount"); $volume = str("$name:$mount");
if ($topLevelVolumes->has($name)) { if ($topLevelVolumes->has($name)) {
$v = $topLevelVolumes->get($name); $v = $topLevelVolumes->get($name);
@@ -2102,7 +2102,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$name = $volume->before(':'); $name = $volume->before(':');
$mount = $volume->after(':'); $mount = $volume->after(':');
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$name = $name."-pr-$pull_request_id"; $name = addPreviewDeploymentSuffix($name, $pull_request_id);
} }
$volume = str("$name:$mount"); $volume = str("$name:$mount");
} }
@@ -2121,7 +2121,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$source = str($source)->replaceFirst('~', $dir); $source = str($source)->replaceFirst('~', $dir);
} }
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$source = $source."-pr-$pull_request_id"; $source = addPreviewDeploymentSuffix($source, $pull_request_id);
} }
if ($read_only) { if ($read_only) {
data_set($volume, 'source', $source.':'.$target.':ro'); data_set($volume, 'source', $source.':'.$target.':ro');
@@ -2130,7 +2130,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
} }
} else { } else {
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$source = $source."-pr-$pull_request_id"; $source = addPreviewDeploymentSuffix($source, $pull_request_id);
} }
if ($read_only) { if ($read_only) {
data_set($volume, 'source', $source.':'.$target.':ro'); data_set($volume, 'source', $source.':'.$target.':ro');
@@ -2182,13 +2182,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$name = $name->replaceFirst('~', $dir); $name = $name->replaceFirst('~', $dir);
} }
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$name = $name."-pr-$pull_request_id"; $name = addPreviewDeploymentSuffix($name, $pull_request_id);
} }
$volume = str("$name:$mount"); $volume = str("$name:$mount");
} else { } else {
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$uuid = $resource->uuid; $uuid = $resource->uuid;
$name = $uuid."-$name-pr-$pull_request_id"; $name = $uuid.'-'.addPreviewDeploymentSuffix($name, $pull_request_id);
$volume = str("$name:$mount"); $volume = str("$name:$mount");
if ($topLevelVolumes->has($name)) { if ($topLevelVolumes->has($name)) {
$v = $topLevelVolumes->get($name); $v = $topLevelVolumes->get($name);
@@ -2230,7 +2230,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$name = $volume->before(':'); $name = $volume->before(':');
$mount = $volume->after(':'); $mount = $volume->after(':');
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$name = $name."-pr-$pull_request_id"; $name = addPreviewDeploymentSuffix($name, $pull_request_id);
} }
$volume = str("$name:$mount"); $volume = str("$name:$mount");
} }
@@ -2258,7 +2258,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
if ($pull_request_id === 0) { if ($pull_request_id === 0) {
$source = $uuid."-$source"; $source = $uuid."-$source";
} else { } else {
$source = $uuid."-$source-pr-$pull_request_id"; $source = $uuid.'-'.addPreviewDeploymentSuffix($source, $pull_request_id);
} }
if ($read_only) { if ($read_only) {
data_set($volume, 'source', $source.':'.$target.':ro'); data_set($volume, 'source', $source.':'.$target.':ro');
@@ -2298,7 +2298,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
if ($pull_request_id !== 0 && count($serviceDependencies) > 0) { if ($pull_request_id !== 0 && count($serviceDependencies) > 0) {
$serviceDependencies = $serviceDependencies->map(function ($dependency) use ($pull_request_id) { $serviceDependencies = $serviceDependencies->map(function ($dependency) use ($pull_request_id) {
return $dependency."-pr-$pull_request_id"; return addPreviewDeploymentSuffix($dependency, $pull_request_id);
}); });
data_set($service, 'depends_on', $serviceDependencies->toArray()); data_set($service, 'depends_on', $serviceDependencies->toArray());
} }
@@ -2692,7 +2692,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
}); });
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$services->each(function ($service, $serviceName) use ($pull_request_id, $services) { $services->each(function ($service, $serviceName) use ($pull_request_id, $services) {
$services[$serviceName."-pr-$pull_request_id"] = $service; $services[addPreviewDeploymentSuffix($serviceName, $pull_request_id)] = $service;
data_forget($services, $serviceName); data_forget($services, $serviceName);
}); });
} }
@@ -3072,3 +3072,17 @@ function parseDockerfileInterval(string $something)
return $seconds; return $seconds;
} }
function addPreviewDeploymentSuffix(string $name, int $pull_request_id = 0): string
{
return ($pull_request_id === 0)? $name : $name.'-pr-'.$pull_request_id;
}
function generateDockerComposeServiceName(mixed $services, int $pullRequestId = 0) : Collection
{
$collection = collect([]);
foreach ($services as $serviceName => $_) {
$collection->put('SERVICE_NAME_'.str($serviceName)->upper(), addPreviewDeploymentSuffix($serviceName,$pullRequestId));
}
return $collection;
}