diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php
index e95c29f72..4783df072 100644
--- a/app/Console/Commands/Init.php
+++ b/app/Console/Commands/Init.php
@@ -253,7 +253,7 @@ class Init extends Command
'save_s3' => false,
'frequency' => '0 0 * * *',
'database_id' => $database->id,
- 'database_type' => \App\Models\StandalonePostgresql::class,
+ 'database_type' => StandalonePostgresql::class,
'team_id' => 0,
]);
}
diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php
index 0bef2dc0f..5e5405a7a 100644
--- a/app/Http/Controllers/Api/ApplicationsController.php
+++ b/app/Http/Controllers/Api/ApplicationsController.php
@@ -146,7 +146,7 @@ class ApplicationsController extends Controller
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.'],
@@ -312,7 +312,7 @@ class ApplicationsController extends Controller
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.'],
@@ -478,7 +478,7 @@ class ApplicationsController extends Controller
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.'],
@@ -781,7 +781,7 @@ class ApplicationsController extends Controller
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.'],
@@ -1024,7 +1024,7 @@ class ApplicationsController extends Controller
'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',
@@ -1230,7 +1230,7 @@ class ApplicationsController extends Controller
'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',
@@ -1470,7 +1470,7 @@ class ApplicationsController extends Controller
'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',
@@ -1793,7 +1793,7 @@ class ApplicationsController extends Controller
$validationRules = [
'docker_registry_image_name' => ['required', 'string', 'max:255', new DockerImageFormat],
'docker_registry_image_tag' => ValidationPatterns::dockerImageTagRules(),
- 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
+ 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|nullable',
];
$validationRules = array_merge(sharedDataApplications(), $validationRules);
$validator = customApiValidator($request->all(), $validationRules);
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index d6a842397..811d0c9bd 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -1388,7 +1388,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
// 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]}");
}
}
@@ -3138,7 +3138,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
'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(
@@ -3170,16 +3170,19 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
// 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)) {
@@ -3389,7 +3392,11 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
// 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;
}
diff --git a/app/Jobs/SendWebhookJob.php b/app/Jobs/SendWebhookJob.php
index 9d2a94606..17517cebb 100644
--- a/app/Jobs/SendWebhookJob.php
+++ b/app/Jobs/SendWebhookJob.php
@@ -2,6 +2,7 @@
namespace App\Jobs;
+use App\Rules\SafeWebhookUrl;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -44,7 +45,7 @@ class SendWebhookJob implements ShouldBeEncrypted, ShouldQueue
{
$validator = Validator::make(
['webhook_url' => $this->webhookUrl],
- ['webhook_url' => ['required', 'url', new \App\Rules\SafeWebhookUrl]]
+ ['webhook_url' => ['required', 'url', new SafeWebhookUrl]]
);
if ($validator->fails()) {
diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php
index 0295aa5fc..89b1b4217 100644
--- a/app/Livewire/Project/Application/General.php
+++ b/app/Livewire/Project/Application/General.php
@@ -154,7 +154,7 @@ class General extends Component
'staticImage' => 'required',
'baseDirectory' => array_merge(['required'], array_slice(ValidationPatterns::directoryPathRules(), 1)),
'publishDirectory' => ValidationPatterns::directoryPathRules(),
- 'portsExposes' => ['required', 'string', 'regex:/^(\d+)(,\d+)*$/'],
+ 'portsExposes' => ['nullable', 'string', 'regex:/^(\d+)(,\d+)*$/'],
'portsMappings' => ValidationPatterns::portMappingRules(),
'customNetworkAliases' => 'nullable',
'dockerfile' => 'nullable',
@@ -212,7 +212,6 @@ class General extends Component
'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.',
'portsExposes.regex' => 'Ports exposes must be a comma-separated list of port numbers (e.g. 3000,3001).',
...ValidationPatterns::portMappingMessages(),
'isStatic.required' => 'The Static setting is required.',
@@ -760,7 +759,7 @@ class General extends Component
$this->resetErrorBag();
- $this->portsExposes = str($this->portsExposes)->replace(' ', '')->trim()->toString();
+ $this->portsExposes = str($this->portsExposes)->replace(' ', '')->trim()->toString() ?: null;
if ($this->portsMappings) {
$this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString();
}
diff --git a/app/Models/Application.php b/app/Models/Application.php
index 3905281d8..a1d34600e 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -2346,7 +2346,7 @@ class Application extends BaseModel
'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()) {
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index e97bc3732..2cf159bfd 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -86,7 +86,7 @@ function format_docker_command_output_to_json($rawOutput): Collection
return $outputLines
->reject(fn ($line) => empty($line))
->map(fn ($outputLine) => json_decode($outputLine, true, flags: JSON_THROW_ON_ERROR));
- } catch (\Throwable) {
+ } catch (Throwable) {
return collect([]);
}
}
@@ -123,7 +123,7 @@ function format_docker_envs_to_json($rawOutput)
return [$env[0] => $env[1]];
});
- } catch (\Throwable) {
+ } catch (Throwable) {
return collect([]);
}
}
@@ -255,12 +255,12 @@ function defaultLabels($id, $name, string $projectName, string $resourceName, st
function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
{
- if ($resource->getMorphClass() === \App\Models\ServiceApplication::class) {
+ if ($resource->getMorphClass() === ServiceApplication::class) {
$uuid = data_get($resource, 'uuid');
$server = data_get($resource, 'service.server');
$environment_variables = data_get($resource, 'service.environment_variables');
$type = $resource->serviceType();
- } elseif ($resource->getMorphClass() === \App\Models\Application::class) {
+ } elseif ($resource->getMorphClass() === Application::class) {
$uuid = data_get($resource, 'uuid');
$server = data_get($resource, 'destination.server');
$environment_variables = data_get($resource, 'environment_variables');
@@ -641,7 +641,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
}
}
}
- } catch (\Throwable) {
+ } catch (Throwable) {
continue;
}
}
@@ -1221,7 +1221,7 @@ function validateComposeFile(string $compose, int $server_id): string|Throwable
$server = Server::ownedByCurrentTeam()->find($server_id);
try {
if (! $server) {
- throw new \Exception('Server not found');
+ throw new Exception('Server not found');
}
$yaml_compose = Yaml::parse($compose);
@@ -1237,7 +1237,7 @@ function validateComposeFile(string $compose, int $server_id): string|Throwable
], $server);
return 'OK';
- } catch (\Throwable $e) {
+ } catch (Throwable $e) {
return $e->getMessage();
} finally {
if (filled($server)) {
@@ -1353,10 +1353,10 @@ function escapeBashDoubleQuoted(?string $value): string
* Generate Docker build arguments from environment variables collection
* Returns only keys (no values) since values are sourced from environment via export
*
- * @param \Illuminate\Support\Collection|array $variables Collection of variables with 'key', 'value', and optionally 'is_multiline'
- * @return \Illuminate\Support\Collection Collection of formatted --build-arg strings (keys only)
+ * @param Collection|array $variables Collection of variables with 'key', 'value', and optionally 'is_multiline'
+ * @return Collection Collection of formatted --build-arg strings (keys only)
*/
-function generateDockerBuildArgs($variables): \Illuminate\Support\Collection
+function generateDockerBuildArgs($variables): Collection
{
$variables = collect($variables);
@@ -1371,7 +1371,7 @@ function generateDockerBuildArgs($variables): \Illuminate\Support\Collection
/**
* Generate Docker environment flags from environment variables collection
*
- * @param \Illuminate\Support\Collection|array $variables Collection of variables with 'key', 'value', and optionally 'is_multiline'
+ * @param Collection|array $variables Collection of variables with 'key', 'value', and optionally 'is_multiline'
* @return string Space-separated environment flags
*/
function generateDockerEnvFlags($variables): string
diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php
index d443c84a4..699704393 100644
--- a/bootstrap/helpers/proxy.php
+++ b/bootstrap/helpers/proxy.php
@@ -4,6 +4,7 @@ use App\Actions\Proxy\SaveProxyConfiguration;
use App\Enums\ProxyTypes;
use App\Models\Application;
use App\Models\Server;
+use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Symfony\Component\Yaml\Yaml;
@@ -137,7 +138,7 @@ function connectProxyToNetworks(Server $server)
* This must be called BEFORE docker compose up since the compose file declares networks as external.
*
* @param Server $server The server to ensure networks on
- * @return \Illuminate\Support\Collection Commands to create networks if they don't exist
+ * @return Collection Commands to create networks if they don't exist
*/
function ensureProxyNetworksExist(Server $server)
{
@@ -215,7 +216,7 @@ function extractCustomProxyCommands(Server $server, string $existing_config): ar
$custom_commands[] = $command;
}
}
- } catch (\Exception $e) {
+ } catch (Exception $e) {
// If we can't parse the config, return empty array
// Silently fail to avoid breaking the proxy regeneration
}
@@ -436,7 +437,7 @@ function getExactTraefikVersionFromContainer(Server $server): ?string
Log::debug("getExactTraefikVersionFromContainer: Server '{$server->name}' (ID: {$server->id}) - Could not detect exact version");
return null;
- } catch (\Exception $e) {
+ } catch (Exception $e) {
Log::error("getExactTraefikVersionFromContainer: Server '{$server->name}' (ID: {$server->id}) - Error: ".$e->getMessage());
return null;
@@ -483,7 +484,7 @@ function getTraefikVersionFromDockerCompose(Server $server): ?string
Log::debug("getTraefikVersionFromDockerCompose: Server '{$server->name}' (ID: {$server->id}) - Image format doesn't match expected pattern: {$image}");
return null;
- } catch (\Exception $e) {
+ } catch (Exception $e) {
Log::error("getTraefikVersionFromDockerCompose: Server '{$server->name}' (ID: {$server->id}) - Error: ".$e->getMessage());
return null;
diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php
index bd7689335..3a516378f 100644
--- a/bootstrap/helpers/remoteProcess.php
+++ b/bootstrap/helpers/remoteProcess.php
@@ -245,7 +245,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
$timestamp = Carbon::parse(data_get($i, 'timestamp'));
try {
$timestamp->setTimezone($serverTimezone);
- } catch (\Exception) {
+ } catch (Exception) {
$timestamp->setTimezone('UTC');
}
data_set($i, 'timestamp', $timestamp->format('Y-M-d H:i:s.u'));
diff --git a/config/purify.php b/config/purify.php
index a5dcabb92..3d181d6eb 100644
--- a/config/purify.php
+++ b/config/purify.php
@@ -1,5 +1,6 @@
[
'driver' => env('CACHE_STORE', env('CACHE_DRIVER', 'file')),
- 'cache' => \Stevebauman\Purify\Cache\CacheDefinitionCache::class,
+ 'cache' => CacheDefinitionCache::class,
],
// 'serializer' => [
diff --git a/database/migrations/2026_03_26_000000_make_ports_exposes_nullable_in_applications_table.php b/database/migrations/2026_03_26_000000_make_ports_exposes_nullable_in_applications_table.php
new file mode 100644
index 000000000..ac7b5cb55
--- /dev/null
+++ b/database/migrations/2026_03_26_000000_make_ports_exposes_nullable_in_applications_table.php
@@ -0,0 +1,22 @@
+string('ports_exposes')->nullable()->change();
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('applications', function (Blueprint $table) {
+ $table->string('ports_exposes')->nullable(false)->default('')->change();
+ });
+ }
+};
diff --git a/database/seeders/SharedEnvironmentVariableSeeder.php b/database/seeders/SharedEnvironmentVariableSeeder.php
index 7a17fbd10..cfd2a3fef 100644
--- a/database/seeders/SharedEnvironmentVariableSeeder.php
+++ b/database/seeders/SharedEnvironmentVariableSeeder.php
@@ -35,7 +35,7 @@ class SharedEnvironmentVariableSeeder extends Seeder
]);
// Add predefined server variables to all existing servers
- $servers = \App\Models\Server::all();
+ $servers = Server::all();
foreach ($servers as $server) {
SharedEnvironmentVariable::firstOrCreate([
'key' => 'COOLIFY_SERVER_UUID',
diff --git a/openapi.json b/openapi.json
index f3bda9ab8..ca445ade0 100644
--- a/openapi.json
+++ b/openapi.json
@@ -79,8 +79,7 @@
"environment_uuid",
"git_repository",
"git_branch",
- "build_pack",
- "ports_exposes"
+ "build_pack"
],
"properties": {
"project_uuid": {
@@ -526,8 +525,7 @@
"github_app_uuid",
"git_repository",
"git_branch",
- "build_pack",
- "ports_exposes"
+ "build_pack"
],
"properties": {
"project_uuid": {
@@ -977,8 +975,7 @@
"private_key_uuid",
"git_repository",
"git_branch",
- "build_pack",
- "ports_exposes"
+ "build_pack"
],
"properties": {
"project_uuid": {
@@ -1775,8 +1772,7 @@
"server_uuid",
"environment_name",
"environment_uuid",
- "docker_registry_image_name",
- "ports_exposes"
+ "docker_registry_image_name"
],
"properties": {
"project_uuid": {
diff --git a/openapi.yaml b/openapi.yaml
index fae8d7110..6182cacd3 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -59,7 +59,6 @@ paths:
- git_repository
- git_branch
- build_pack
- - ports_exposes
properties:
project_uuid:
type: string
@@ -344,7 +343,6 @@ paths:
- git_repository
- git_branch
- build_pack
- - ports_exposes
properties:
project_uuid:
type: string
@@ -632,7 +630,6 @@ paths:
- git_repository
- git_branch
- build_pack
- - ports_exposes
properties:
project_uuid:
type: string
@@ -1141,7 +1138,6 @@ paths:
- environment_name
- environment_uuid
- docker_registry_image_name
- - ports_exposes
properties:
project_uuid:
type: string
diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php
index 7cc595896..190f4262a 100644
--- a/resources/views/livewire/project/application/general.blade.php
+++ b/resources/views/livewire/project/application/general.blade.php
@@ -500,6 +500,13 @@
@endif
@endif
+ @if ((empty($portsExposes) || $portsExposes === '0') && !empty($fqdn))
+