From 266d414a0418bd8e55699e9469ce8a4654add01a Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Wed, 9 Sep 2026 13:18:14 +0200 Subject: [PATCH] feat(docker): add container name prefix for generated container names --- app/Models/ApplicationSetting.php | 14 ++++++++++++++ .../ApplicationConfigurationSnapshot.php | 1 + bootstrap/helpers/docker.php | 2 +- ...e_prefix_to_application_settings_table.php | 18 ++++++++++++++++++ ...plicationDeploymentContainerNamingTest.php | 19 +++++++++++++++++++ 5 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 database/migrations/2026_09_08_214513_add_custom_container_name_prefix_to_application_settings_table.php diff --git a/app/Models/ApplicationSetting.php b/app/Models/ApplicationSetting.php index 91c38b8790..60fb9b2336 100644 --- a/app/Models/ApplicationSetting.php +++ b/app/Models/ApplicationSetting.php @@ -32,6 +32,7 @@ use OpenApi\Attributes as OA; 'is_stripprefix_enabled' => ['type' => 'boolean'], 'connect_to_docker_network' => ['type' => 'boolean'], 'custom_internal_name' => ['type' => 'string', 'nullable' => true], + 'custom_container_name_prefix' => ['type' => 'string', 'nullable' => true], 'is_container_label_escape_enabled' => ['type' => 'boolean'], 'is_env_sorting_enabled' => ['type' => 'boolean'], 'is_container_label_readonly_enabled' => ['type' => 'boolean'], @@ -106,6 +107,7 @@ class ApplicationSetting extends Model 'is_stripprefix_enabled', 'connect_to_docker_network', 'custom_internal_name', + 'custom_container_name_prefix', 'is_container_label_escape_enabled', 'is_env_sorting_enabled', 'is_container_label_readonly_enabled', @@ -121,6 +123,18 @@ class ApplicationSetting extends Model 'stop_grace_period', ]; + /** + * Like custom container names, a prefix must be unique per server so that uuid, custom container + * name and prefix each identify one container when resolving connections. + */ + public static function isContainerNamePrefixInUse(string $prefix, Server $server, ?int $ignoreApplicationId = null): bool + { + return $server->applications()->contains(function (Application $application) use ($prefix, $ignoreApplicationId) { + return $application->id !== $ignoreApplicationId + && in_array($prefix, [$application->uuid, $application->settings->custom_container_name_prefix, $application->settings->custom_internal_name], true); + }); + } + public function stopGracePeriodSeconds(): int { if ( diff --git a/app/Services/DeploymentConfiguration/ApplicationConfigurationSnapshot.php b/app/Services/DeploymentConfiguration/ApplicationConfigurationSnapshot.php index e3ba77163d..184aa01eb3 100644 --- a/app/Services/DeploymentConfiguration/ApplicationConfigurationSnapshot.php +++ b/app/Services/DeploymentConfiguration/ApplicationConfigurationSnapshot.php @@ -170,6 +170,7 @@ class ApplicationConfigurationSnapshot $this->item('custom_network_aliases', 'Network aliases', $this->application->custom_network_aliases, 'redeploy'), $this->item('connect_to_docker_network', 'Connect to Docker network', data_get($this->application, 'settings.connect_to_docker_network'), 'redeploy'), $this->item('custom_internal_name', 'Custom container name', data_get($this->application, 'settings.custom_internal_name'), 'redeploy'), + $this->item('custom_container_name_prefix', 'Container name prefix', data_get($this->application, 'settings.custom_container_name_prefix'), 'redeploy'), $this->item('is_consistent_container_name_enabled', 'Consistent container name', data_get($this->application, 'settings.is_consistent_container_name_enabled'), 'redeploy'), $this->item('is_container_label_escape_enabled', 'Escape container labels', data_get($this->application, 'settings.is_container_label_escape_enabled'), 'redeploy'), $this->item('is_container_label_readonly_enabled', 'Read-only container labels', data_get($this->application, 'settings.is_container_label_readonly_enabled'), 'redeploy'), diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 38cf3fefc4..3c87882d6f 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -358,7 +358,7 @@ function generateApplicationContainerName(Application $application, $pull_reques return $name; } - return $application->uuid.'-'.$now; + return ($application->settings->custom_container_name_prefix ?: $application->uuid).'-'.$now; } } diff --git a/database/migrations/2026_09_08_214513_add_custom_container_name_prefix_to_application_settings_table.php b/database/migrations/2026_09_08_214513_add_custom_container_name_prefix_to_application_settings_table.php new file mode 100644 index 0000000000..d49c1d57aa --- /dev/null +++ b/database/migrations/2026_09_08_214513_add_custom_container_name_prefix_to_application_settings_table.php @@ -0,0 +1,18 @@ +string('custom_container_name_prefix')->nullable(); + }); + } +}; diff --git a/tests/Unit/ApplicationDeploymentContainerNamingTest.php b/tests/Unit/ApplicationDeploymentContainerNamingTest.php index 1a268d4f38..385e547eb6 100644 --- a/tests/Unit/ApplicationDeploymentContainerNamingTest.php +++ b/tests/Unit/ApplicationDeploymentContainerNamingTest.php @@ -64,3 +64,22 @@ it('recognises generated container names in both timestamp formats', function () ->and(isGeneratedContainerName('application-uuid-pr-42'))->toBeFalse() ->and(isGeneratedContainerName('my-api'))->toBeFalse(); }); + +function applicationWithContainerNamePrefix(string $prefix = 'my-api', bool $consistent = false): Application +{ + $application = new Application; + $application->forceFill(['uuid' => 'application-uuid']); + $application->setRelation('settings', new ApplicationSetting([ + 'custom_container_name_prefix' => $prefix, + 'is_consistent_container_name_enabled' => $consistent, + ])); + + return $application; +} + +it('uses the container name prefix for generated container names only', function () { + expect(generateApplicationContainerName(applicationWithContainerNamePrefix()))->toMatch('/^my-api-\d{8}T\d{6}$/') + ->and(generateApplicationContainerName(applicationWithContainerNamePrefix('')))->toMatch('/^application-uuid-\d{8}T\d{6}$/') + ->and(generateApplicationContainerName(applicationWithContainerNamePrefix(consistent: true)))->toBe('application-uuid') + ->and(generateApplicationContainerName(applicationWithContainerNamePrefix(), 42))->toBe('application-uuid-pr-42'); +});