feat(application): make ports_exposes optional for portless apps

This commit is contained in:
ShadowArcanist 2026-03-26 13:20:41 +05:30
parent ad95d65aca
commit 9c8e5645b4
6 changed files with 60 additions and 25 deletions

View file

@ -144,7 +144,7 @@ public function applications(Request $request)
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'git_repository', 'git_branch', 'build_pack', 'ports_exposes'],
required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'git_repository', 'git_branch', 'build_pack'],
properties: [
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
@ -309,7 +309,7 @@ public function create_public_application(Request $request)
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'github_app_uuid', 'git_repository', 'git_branch', 'build_pack', 'ports_exposes'],
required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'github_app_uuid', 'git_repository', 'git_branch', 'build_pack'],
properties: [
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
@ -474,7 +474,7 @@ public function create_private_gh_app_application(Request $request)
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'private_key_uuid', 'git_repository', 'git_branch', 'build_pack', 'ports_exposes'],
required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'private_key_uuid', 'git_repository', 'git_branch', 'build_pack'],
properties: [
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
@ -776,7 +776,7 @@ public function create_dockerfile_application(Request $request)
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'docker_registry_image_name', 'ports_exposes'],
required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'docker_registry_image_name'],
properties: [
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
@ -1114,7 +1114,7 @@ private function create_application(Request $request, $type)
'git_repository' => ['string', 'required', new ValidGitRepositoryUrl],
'git_branch' => ['string', 'required', new ValidGitBranch],
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|nullable',
'docker_compose_domains' => 'array|nullable',
'docker_compose_domains.*' => 'array:name,domain',
'docker_compose_domains.*.name' => 'string|required',
@ -1307,7 +1307,7 @@ private function create_application(Request $request, $type)
'git_repository' => 'string|required',
'git_branch' => ['string', 'required', new ValidGitBranch],
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|nullable',
'github_app_uuid' => 'string|required',
'watch_paths' => 'string|nullable',
'docker_compose_domains' => 'array|nullable',
@ -1534,7 +1534,7 @@ private function create_application(Request $request, $type)
'git_repository' => ['string', 'required', new ValidGitRepositoryUrl],
'git_branch' => ['string', 'required', new ValidGitBranch],
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|nullable',
'private_key_uuid' => 'string|required',
'watch_paths' => 'string|nullable',
'docker_compose_domains' => 'array|nullable',
@ -1835,7 +1835,7 @@ private function create_application(Request $request, $type)
$validationRules = [
'docker_registry_image_name' => 'string|required',
'docker_registry_image_tag' => 'string',
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|nullable',
];
$validationRules = array_merge(sharedDataApplications(), $validationRules);
$validator = customApiValidator($request->all(), $validationRules);

View file

@ -1282,7 +1282,7 @@ private function generate_runtime_environment_variables()
// Add PORT if not exists, use the first port as default
if ($this->build_pack !== 'dockercompose') {
if ($this->application->environment_variables->where('key', 'PORT')->isEmpty()) {
if ($this->application->environment_variables->where('key', 'PORT')->isEmpty() && ! empty($ports)) {
$envs->push("PORT={$ports[0]}");
}
}
@ -2585,7 +2585,7 @@ private function generate_compose_file()
'image' => $this->production_image_name,
'container_name' => $this->container_name,
'restart' => RESTART_MODE,
'expose' => $ports,
...(!empty($ports) ? ['expose' => $ports] : []),
'networks' => [
$this->destination->network => [
'aliases' => array_merge(
@ -2617,16 +2617,19 @@ private function generate_compose_file()
// If custom_healthcheck_found is true, the Dockerfile's HEALTHCHECK will be used
// If healthcheck is disabled, no healthcheck will be added
if (! $this->application->custom_healthcheck_found && ! $this->application->isHealthcheckDisabled()) {
$docker_compose['services'][$this->container_name]['healthcheck'] = [
'test' => [
'CMD-SHELL',
$this->generate_healthcheck_commands(),
],
'interval' => $this->application->health_check_interval.'s',
'timeout' => $this->application->health_check_timeout.'s',
'retries' => $this->application->health_check_retries,
'start_period' => $this->application->health_check_start_period.'s',
];
$healthcheck_command = $this->generate_healthcheck_commands();
if ($healthcheck_command !== null) {
$docker_compose['services'][$this->container_name]['healthcheck'] = [
'test' => [
'CMD-SHELL',
$healthcheck_command,
],
'interval' => $this->application->health_check_interval.'s',
'timeout' => $this->application->health_check_timeout.'s',
'retries' => $this->application->health_check_retries,
'start_period' => $this->application->health_check_start_period.'s',
];
}
}
if (! is_null($this->application->limits_cpuset)) {
@ -2836,7 +2839,11 @@ private function generate_healthcheck_commands()
// HTTP type healthcheck (default)
if (! $this->application->health_check_port) {
$health_check_port = (int) $this->application->ports_exposes_array[0];
if (! empty($this->application->ports_exposes_array)) {
$health_check_port = (int) $this->application->ports_exposes_array[0];
} else {
return null;
}
} else {
$health_check_port = (int) $this->application->health_check_port;
}

View file

@ -153,7 +153,7 @@ protected function rules(): array
'staticImage' => 'required',
'baseDirectory' => array_merge(['required'], array_slice(ValidationPatterns::directoryPathRules(), 1)),
'publishDirectory' => ValidationPatterns::directoryPathRules(),
'portsExposes' => 'required',
'portsExposes' => 'nullable',
'portsMappings' => 'nullable',
'customNetworkAliases' => 'nullable',
'dockerfile' => 'nullable',
@ -208,7 +208,6 @@ protected function messages(): array
'buildPack.required' => 'The Build Pack field is required.',
'staticImage.required' => 'The Static Image field is required.',
'baseDirectory.required' => 'The Base Directory field is required.',
'portsExposes.required' => 'The Exposed Ports field is required.',
'isStatic.required' => 'The Static setting is required.',
'isStatic.boolean' => 'The Static setting must be true or false.',
'isSpa.required' => 'The SPA setting is required.',

View file

@ -2147,7 +2147,7 @@ public function setConfig($config)
'config.build_pack' => 'required|string',
'config.base_directory' => 'required|string',
'config.publish_directory' => 'required|string',
'config.ports_exposes' => 'required|string',
'config.ports_exposes' => 'nullable|string',
'config.settings.is_static' => 'required|boolean',
]);
if ($deepValidator->fails()) {

View file

@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->string('ports_exposes')->nullable()->change();
});
}
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->string('ports_exposes')->nullable(false)->default('')->change();
});
}
};

View file

@ -492,6 +492,13 @@ class="flex items-start gap-2 p-4 mb-4 text-sm rounded-lg bg-blue-50 dark:bg-blu
</div>
@endif
@endif
@if (empty($portsExposes) || $portsExposes === '0')
<x-callout type="info" title="No ports exposed" class="mb-4">
This application does not expose any ports and will not be reachable through the proxy or your domains.
This behavior is normal for background workers, bots, or scheduled tasks.
If your application needs to handle HTTP traffic, please specify the port(s) it listens on.
</x-callout>
@endif
<div class="flex flex-col gap-2 xl:flex-row">
@if ($isStatic || $buildPack === 'static')
<x-forms.input id="portsExposes" label="Ports Exposes" readonly
@ -502,7 +509,7 @@ class="flex items-start gap-2 p-4 mb-4 text-sm rounded-lg bg-blue-50 dark:bg-blu
helper="Readonly labels are disabled. You can set the ports manually in the labels section."
x-bind:disabled="!canUpdate" />
@else
<x-forms.input placeholder="3000,3001" id="portsExposes" label="Ports Exposes" required
<x-forms.input placeholder="3000,3001" id="portsExposes" label="Ports Exposes"
helper="A comma separated list of ports your application uses. The first port will be used as default healthcheck port if nothing defined in the Healthcheck menu. Be sure to set this correctly."
x-bind:disabled="!canUpdate" />
@endif