Merge remote-tracking branch 'origin/next' into jean/port-exposes-improvement

This commit is contained in:
Andras Bacsai committed 2026-06-03 10:32:57 +02:00
commit 07f381b88c
766 files changed
+43286 -12686

No files matched your search

@@ -5,19 +5,7 @@
</div>
<div>Advanced configuration for your application.</div>
<div class="flex flex-col gap-1 pt-4">
<h3>General</h3>
@if ($application->git_based())
<x-forms.checkbox helper="Automatically deploy new commits based on Git webhooks." instantSave
id="isAutoDeployEnabled" label="Auto Deploy" canGate="update" :canResource="$application" />
<x-forms.checkbox
helper="Allow to automatically deploy Preview Deployments for all opened PR's.<br><br>Closing a PR will delete Preview Deployments."
instantSave id="isPreviewDeploymentsEnabled" label="Preview Deployments" canGate="update"
:canResource="$application" />
<x-forms.checkbox
helper="When enabled, anyone can trigger PR deployments. When disabled, only repository members, collaborators, and contributors can trigger PR deployments."
instantSave id="isPrDeploymentsPublicEnabled" label="Allow Public PR Deployments" canGate="update"
:canResource="$application" :disabled="!$isPreviewDeploymentsEnabled" />
@endif
<h3>Build</h3>
<x-forms.checkbox helper="Disable Docker build cache on every deployment." instantSave
id="disableBuildCache" label="Disable Build Cache" canGate="update" :canResource="$application" />
<x-forms.checkbox
@@ -29,6 +17,55 @@
instantSave id="includeSourceCommitInBuild" label="Include Source Commit in Build" canGate="update"
:canResource="$application" />
<h3 class="pt-4">Container</h3>
<x-forms.checkbox
helper="The deployed container will have the same name ({{ $application->uuid }}). <span class='font-bold dark:text-warning'>You will lose the rolling update feature!</span>"
instantSave id="isConsistentContainerNameEnabled" label="Consistent Container Names" canGate="update"
:canResource="$application" />
@if ($isConsistentContainerNameEnabled === false)
<form class="flex items-end gap-2 " wire:submit.prevent='saveCustomName'>
<x-forms.input
helper="You can add a custom name for your container.<br><br>The name will be converted to slug format when you save it. <span class='font-bold dark:text-warning'>You will lose the rolling update feature!</span>"
instantSave id="customInternalName" label="Custom Container Name" canGate="update"
:canResource="$application" />
<x-forms.button canGate="update" :canResource="$application" type="submit">Save</x-forms.button>
</form>
@endif
@if ($application->git_based())
<h3 class="pt-4">Deployment</h3>
<x-forms.checkbox helper="Automatically deploy new commits based on Git webhooks." instantSave
id="isAutoDeployEnabled" label="Auto Deploy" canGate="update" :canResource="$application" />
<x-forms.checkbox
helper="Allow to automatically deploy Preview Deployments for all opened PR's.<br><br>Closing a PR will delete Preview Deployments."
instantSave id="isPreviewDeploymentsEnabled" label="Preview Deployments" canGate="update"
:canResource="$application" />
<x-forms.checkbox
helper="When enabled, anyone can trigger PR deployments. When disabled, fork PRs are blocked and only repository owners, members, and collaborators can trigger PR deployments."
instantSave id="isPrDeploymentsPublicEnabled" label="Allow Public PR Deployments" canGate="update"
:canResource="$application" :disabled="!$isPreviewDeploymentsEnabled" />
<h3 class="pt-4">Git</h3>
<x-forms.checkbox instantSave id="isGitSubmodulesEnabled" label="Submodules"
helper="Allow Git Submodules during build process." canGate="update" :canResource="$application" />
<x-forms.checkbox instantSave id="isGitLfsEnabled" label="LFS"
helper="Allow Git LFS during build process." canGate="update" :canResource="$application" />
<x-forms.checkbox instantSave id="isGitShallowCloneEnabled" label="Shallow Clone"
helper="Use shallow cloning (--depth=1) to speed up deployments by only fetching the latest commit history. This reduces clone time and resource usage, especially for large repositories."
canGate="update" :canResource="$application" />
@endif
@if ($application->build_pack === 'dockercompose')
<h3 class="pt-4">Docker Compose</h3>
<x-forms.checkbox instantSave id="isRawComposeDeploymentEnabled" label="Raw Compose Deployment"
helper="WARNING: Advanced use cases only. Your docker compose file will be deployed as-is. Nothing is modified by Coolify. You need to configure the proxy parts. More info in the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/compose#raw-docker-compose-deployment'>documentation.</a>"
canGate="update" :canResource="$application" />
<x-forms.checkbox instantSave id="isConnectToDockerNetworkEnabled" label="Connect To Predefined Network"
helper="By default, you do not reach the Coolify defined networks.<br>Starting a docker compose based resource will have an internal network. <br>If you connect to a Coolify defined network, you maybe need to use different internal DNS names to connect to a resource.<br><br>For more information, check <a class='underline dark:text-white' target='_blank' href='https://coolify.io/docs/knowledge-base/docker/compose#connect-to-predefined-networks'>this</a>."
canGate="update" :canResource="$application" />
@endif
<h3 class="pt-4">Proxy</h3>
@if ($application->settings->is_container_label_readonly_enabled)
<x-forms.checkbox
helper="Your application will be available only on https if your domain starts with https://..."
@@ -49,45 +86,24 @@
helper="Readonly labels are disabled. You need to set the labels in the labels section." disabled
instantSave id="isStripprefixEnabled" label="Strip Prefixes" canGate="update" :canResource="$application" />
@endif
@if ($application->build_pack === 'dockercompose')
<h3>Docker Compose</h3>
<x-forms.checkbox instantSave id="isRawComposeDeploymentEnabled" label="Raw Compose Deployment"
helper="WARNING: Advanced use cases only. Your docker compose file will be deployed as-is. Nothing is modified by Coolify. You need to configure the proxy parts. More info in the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/compose#raw-docker-compose-deployment'>documentation.</a>"
canGate="update" :canResource="$application" />
@endif
<h3 class="pt-4">Container Names</h3>
<x-forms.checkbox
helper="The deployed container will have the same name ({{ $application->uuid }}). <span class='font-bold dark:text-warning'>You will lose the rolling update feature!</span>"
instantSave id="isConsistentContainerNameEnabled" label="Consistent Container Names" canGate="update"
:canResource="$application" />
@if ($isConsistentContainerNameEnabled === false)
<form class="flex items-end gap-2 " wire:submit.prevent='saveCustomName'>
<x-forms.input
helper="You can add a custom name for your container.<br><br>The name will be converted to slug format when you save it. <span class='font-bold dark:text-warning'>You will lose the rolling update feature!</span>"
instantSave id="customInternalName" label="Custom Container Name" canGate="update"
:canResource="$application" />
<x-forms.button canGate="update" :canResource="$application" type="submit">Save</x-forms.button>
</form>
@endif
@if ($application->build_pack === 'dockercompose')
<h3 class="pt-4">Network</h3>
<x-forms.checkbox instantSave id="isConnectToDockerNetworkEnabled" label="Connect To Predefined Network"
helper="By default, you do not reach the Coolify defined networks.<br>Starting a docker compose based resource will have an internal network. <br>If you connect to a Coolify defined network, you maybe need to use different internal DNS names to connect to a resource.<br><br>For more information, check <a class='underline dark:text-white' target='_blank' href='https://coolify.io/docs/knowledge-base/docker/compose#connect-to-predefined-networks'>this</a>."
canGate="update" :canResource="$application" />
@endif
<h3 class="pt-4">Operations</h3>
<form class="flex items-end gap-2" wire:submit.prevent='saveStopGracePeriod'>
<x-forms.input
type="number"
id="stopGracePeriod"
label="Stop Grace Period (seconds)"
placeholder="{{ DEFAULT_STOP_GRACE_PERIOD_SECONDS }}"
helper="How long to wait for graceful shutdown during rolling updates, manual stops, and restarts. Applies to all containers for this application. Default: {{ DEFAULT_STOP_GRACE_PERIOD_SECONDS }} seconds. Range: {{ MIN_STOP_GRACE_PERIOD_SECONDS }}-{{ MAX_STOP_GRACE_PERIOD_SECONDS }} seconds (1 hour)."
min="{{ MIN_STOP_GRACE_PERIOD_SECONDS }}"
max="{{ MAX_STOP_GRACE_PERIOD_SECONDS }}"
canGate="update"
:canResource="$application"
/>
<x-forms.button canGate="update" :canResource="$application" type="submit">Save</x-forms.button>
</form>
<h3 class="pt-4">Logs</h3>
<x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings."
instantSave id="isLogDrainEnabled" label="Drain Logs" canGate="update" :canResource="$application" />
@if ($application->git_based())
<h3>Git</h3>
<x-forms.checkbox instantSave id="isGitSubmodulesEnabled" label="Submodules"
helper="Allow Git Submodules during build process." canGate="update" :canResource="$application" />
<x-forms.checkbox instantSave id="isGitLfsEnabled" label="LFS"
helper="Allow Git LFS during build process." canGate="update" :canResource="$application" />
<x-forms.checkbox instantSave id="isGitShallowCloneEnabled" label="Shallow Clone"
helper="Use shallow cloning (--depth=1) to speed up deployments by only fetching the latest commit history. This reduces clone time and resource usage, especially for large repositories."
canGate="update" :canResource="$application" />
@endif
</div>
</div>
@@ -14,7 +14,8 @@
href="{{ route('project.application.advanced', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Advanced</span></a>
@if ($application->destination->server->isSwarm())
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.swarm', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Swarm Configuration</span></a>
href="{{ route('project.application.swarm', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Swarm</span>
</a>
@endif
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.environment-variables', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Environment Variables</span></a>
@@ -26,27 +27,13 @@
@endif
<a class="sub-menu-item flex items-center gap-2" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.servers', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Servers</span>
@if ($application->server_status == false)
<span title="One or more servers are unreachable or misconfigured.">
<svg class="w-4 h-4 text-error" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16" />
</svg>
</span>
@elseif ($application->additional_servers()->exists() && str($application->status)->contains('degraded'))
<span title="Application is in degraded state across multiple servers.">
<svg class="w-4 h-4 text-error" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16" />
</svg>
</span>
@endif
<livewire:project.application.server-status-badge :application="$application" />
</a>
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
<a @class(['sub-menu-item', 'menu-item-active' => str($currentRoute)->startsWith('project.application.scheduled-tasks')]) {{ wireNavigate() }}
href="{{ route('project.application.scheduled-tasks.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Scheduled Tasks</span></a>
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.webhooks', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Webhooks</span></a>
@if ($application->git_based())
@if ($application->git_based() || $application->build_pack === 'dockerimage')
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.preview-deployments', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Preview Deployments</span></a>
@endif
@@ -84,6 +71,8 @@
<livewire:project.shared.destination :resource="$application" />
@elseif ($currentRoute === 'project.application.scheduled-tasks.show')
<livewire:project.shared.scheduled-task.all :resource="$application" />
@elseif ($currentRoute === 'project.application.scheduled-tasks')
<livewire:project.shared.scheduled-task.show />
@elseif ($currentRoute === 'project.application.webhooks')
<livewire:project.shared.webhooks :resource="$application" />
@elseif ($currentRoute === 'project.application.preview-deployments')
@@ -1,4 +1,4 @@
<div>
<div class="flex h-[calc(100vh-10rem)] min-h-[50rem] flex-col overflow-hidden">
<x-slot:title>
{{ data_get_str($application, 'name')->limit(10) }} > Deployment | Coolify
</x-slot>
@@ -9,6 +9,13 @@
fullscreen: @entangle('fullscreen'),
alwaysScroll: {{ $isKeepAliveOn ? 'true' : 'false' }},
rafId: null,
scrollTimeout: null,
scrollDebounce: null,
isScrolling: false,
destroyed: false,
morphUpdatedCleanup: null,
deploymentFinishedCleanup: null,
lastTouchY: 0,
showTimestamps: true,
searchQuery: '',
matchCount: 0,
@@ -17,17 +24,76 @@
this.fullscreen = !this.fullscreen;
},
scrollToBottom() {
const logsContainer = document.getElementById('logsContainer');
if (this.destroyed) return;
const logsContainer = this.$root.querySelector('#logsContainer');
if (logsContainer) {
this.isScrolling = true;
logsContainer.scrollTop = logsContainer.scrollHeight;
requestAnimationFrame(() => { this.isScrolling = false; });
}
},
scheduleScroll() {
cancelScrollLoop() {
if (this.rafId) {
cancelAnimationFrame(this.rafId);
this.rafId = null;
}
if (this.scrollTimeout) {
clearTimeout(this.scrollTimeout);
this.scrollTimeout = null;
}
if (this.scrollDebounce) {
clearTimeout(this.scrollDebounce);
this.scrollDebounce = null;
}
},
disableFollow() {
if (!this.alwaysScroll) return;
this.alwaysScroll = false;
this.cancelScrollLoop();
},
handleWheel(event) {
if (this.alwaysScroll && event.deltaY < 0) {
this.disableFollow();
}
},
handleTouchStart(event) {
this.lastTouchY = event.touches[0].clientY;
},
handleTouchMove(event) {
if (!this.alwaysScroll) return;
const currentY = event.touches[0].clientY;
if (currentY > this.lastTouchY) {
this.disableFollow();
}
this.lastTouchY = currentY;
},
handleKeyScroll(event) {
if (!this.alwaysScroll) return;
const upKeys = ['ArrowUp', 'PageUp', 'Home'];
if (upKeys.includes(event.key)) {
this.disableFollow();
}
},
handleScroll(event) {
if (this.isScrolling || this.destroyed) return;
const el = event.target;
clearTimeout(this.scrollDebounce);
this.scrollDebounce = setTimeout(() => {
if (this.destroyed) return;
const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
if (!this.alwaysScroll && distanceFromBottom <= 10) {
this.alwaysScroll = true;
this.scheduleScroll();
}
}, 150);
},
scheduleScroll() {
if (!this.alwaysScroll || this.destroyed) return;
this.rafId = requestAnimationFrame(() => {
if (!this.alwaysScroll || this.destroyed) return;
this.scrollToBottom();
if (this.alwaysScroll) {
setTimeout(() => this.scheduleScroll(), 250);
if (this.alwaysScroll && !this.destroyed) {
this.scrollTimeout = setTimeout(() => this.scheduleScroll(), 250);
}
});
},
@@ -36,10 +102,7 @@
if (this.alwaysScroll) {
this.scheduleScroll();
} else {
if (this.rafId) {
cancelAnimationFrame(this.rafId);
this.rafId = null;
}
this.cancelScrollLoop();
}
},
hasActiveLogSelection() {
@@ -107,9 +170,9 @@
this.matchCount = query ? count : 0;
},
downloadLogs() {
collectVisibleLogs() {
const logs = document.getElementById('logs');
if (!logs) return;
if (!logs) return '';
const visibleLines = logs.querySelectorAll('[data-log-line]:not(.hidden)');
let content = '';
visibleLines.forEach(line => {
@@ -118,6 +181,17 @@
content += text + String.fromCharCode(10);
}
});
return content;
},
copyLogs() {
const content = this.collectVisibleLogs();
if (!content) return;
navigator.clipboard.writeText(content);
Livewire.dispatch('success', ['Logs copied to clipboard.']);
},
downloadLogs() {
const content = this.collectVisibleLogs();
if (!content) return;
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
@@ -130,10 +204,7 @@
stopScroll() {
this.scrollToBottom();
this.alwaysScroll = false;
if (this.rafId) {
cancelAnimationFrame(this.rafId);
this.rafId = null;
}
this.cancelScrollLoop();
},
init() {
// Watch search query changes
@@ -141,21 +212,26 @@
this.applySearch();
});
// Apply search after Livewire updates
Livewire.hook('morph.updated', ({ el }) => {
if (el.id === 'logs') {
this.$nextTick(() => {
this.applySearch();
if (this.alwaysScroll) {
this.scrollToBottom();
}
});
}
// Apply search after Livewire updates.
// Livewire.hook() returns an unregister fn; keep it for destroy().
this.morphUpdatedCleanup = Livewire.hook('morph.updated', ({ el }) => {
if (this.destroyed) return;
if (el.id !== 'logs' || !this.$root.contains(el)) return;
this.$nextTick(() => {
if (this.destroyed) return;
this.applySearch();
if (this.alwaysScroll) {
this.scrollToBottom();
}
});
});
// Stop auto-scroll when deployment finishes
Livewire.on('deploymentFinished', () => {
// Stop auto-scroll when deployment finishes.
// Livewire.on() returns an unregister fn; keep it for destroy().
this.deploymentFinishedCleanup = Livewire.on('deploymentFinished', () => {
if (this.destroyed) return;
setTimeout(() => {
if (this.destroyed) return;
this.stopScroll();
}, 500);
});
@@ -164,14 +240,32 @@
if (this.alwaysScroll) {
this.scheduleScroll();
}
},
destroy() {
// Runs when Alpine tears the component down (wire:navigate away).
this.destroyed = true;
this.alwaysScroll = false;
this.cancelScrollLoop();
if (this.scrollDebounce) {
clearTimeout(this.scrollDebounce);
this.scrollDebounce = null;
}
if (typeof this.morphUpdatedCleanup === 'function') {
this.morphUpdatedCleanup();
this.morphUpdatedCleanup = null;
}
if (typeof this.deploymentFinishedCleanup === 'function') {
this.deploymentFinishedCleanup();
this.deploymentFinishedCleanup = null;
}
}
}">
}" class="flex flex-1 min-h-0 flex-col overflow-hidden">
<livewire:project.application.deployment-navbar
:application_deployment_queue="$application_deployment_queue" />
<div id="screen" :class="fullscreen ? 'fullscreen flex flex-col' : 'mt-4 relative'">
<div id="screen" :class="fullscreen ? 'fullscreen flex flex-col' : 'mt-4 flex flex-1 min-h-0 flex-col overflow-hidden'">
<div @if ($isKeepAliveOn) wire:poll.2000ms="polling" @endif
class="flex flex-col w-full bg-white dark:text-white dark:bg-coolgray-100 dark:border-coolgray-300"
:class="fullscreen ? 'h-full' : 'border border-dotted rounded-sm'">
class="flex min-h-0 flex-col w-full overflow-hidden bg-white dark:text-white dark:bg-coolgray-100 dark:border-coolgray-300"
:class="fullscreen ? 'h-full' : 'flex-1 border border-dotted rounded-sm'">
<div
class="flex flex-wrap items-center justify-between gap-2 px-4 py-2 border-b dark:border-coolgray-300 border-neutral-200 shrink-0">
<div class="flex items-center gap-3">
@@ -210,12 +304,7 @@
</div>
<div class="flex flex-wrap items-center gap-1">
<button
x-on:click="
$wire.copyLogs().then(logs => {
navigator.clipboard.writeText(logs);
Livewire.dispatch('success', ['Logs copied to clipboard.']);
});
"
x-on:click="copyLogs()"
title="Copy Logs"
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
@@ -327,10 +416,10 @@
</div>
</div>
</div>
<div id="logsContainer"
class="flex flex-col overflow-y-auto p-2 px-4 min-h-4 scrollbar"
:class="fullscreen ? 'flex-1' : 'max-h-[30rem]'">
<div id="logs" class="flex flex-col font-mono">
<div id="logsContainer" @scroll="handleScroll" @wheel="handleWheel"
@touchstart="handleTouchStart" @touchmove="handleTouchMove" @keydown="handleKeyScroll" tabindex="0"
class="flex min-h-40 flex-1 flex-col overflow-y-auto p-2 px-4 scrollbar">
<div id="logs" class="flex flex-col font-logs">
<div x-show="searchQuery.trim() && matchCount === 0"
class="text-gray-500 dark:text-gray-400 py-2">
No matches found.
@@ -356,11 +445,11 @@
])>{{ $lineContent }}</span>
</div>
@empty
<span class="font-mono text-neutral-400 mb-2">No logs yet.</span>
<span class="font-logs text-neutral-400 mb-2">No logs yet.</span>
@endforelse
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -12,6 +12,9 @@
<div>{{ $application->compose_parsing_version }}</div>
@endif
<x-forms.button canGate="update" :canResource="$application" type="submit">Save</x-forms.button>
<x-modal-input title="Resource Details" buttonTitle="Details">
<livewire:project.shared.resource-details :resource="$application" />
</x-modal-input>
@if ($buildPack === 'dockercompose')
<x-forms.button canGate="update" :canResource="$application" wire:target='initLoadingCompose'
x-on:click="$wire.dispatch('loadCompose', false)">
@@ -32,6 +35,7 @@
<x-forms.select x-bind:disabled="shouldDisable()" wire:model.live="buildPack" label="Build Pack"
required>
<option value="nixpacks">Nixpacks</option>
<option value="railpack">Railpack (Beta)</option>
<option value="static">Static</option>
<option value="dockerfile">Dockerfile</option>
<option value="dockercompose">Docker Compose</option>
@@ -60,7 +64,7 @@
@if (!isDatabaseImage(data_get($service, 'image')))
<div class="flex items-end gap-2">
<x-forms.input
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- https://app.coolify.io,https://cloud.coolify.io/dashboard<br>- https://app.coolify.io/api/v3<br>- https://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container.<br>- https://app.coolify.io:8080/api -> app.coolify.io/api will point to port 8080 inside the container."
label="Domains for {{ $serviceName }}"
id="parsedServiceDomains.{{ str($serviceName)->replace('-', '_')->replace('.', '_') }}.domain"
x-bind:disabled="shouldDisable()"></x-forms.input>
@@ -114,7 +118,7 @@
x-bind:disabled="!canUpdate" />
@else
<x-forms.input placeholder="https://coolify.io" wire:model="fqdn" label="Domains"
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- https://app.coolify.io,https://cloud.coolify.io/dashboard<br>- https://app.coolify.io/api/v3<br>- https://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container.<br>- https://app.coolify.io:8080/api -> app.coolify.io/api will point to port 8080 inside the container."
x-bind:disabled="!canUpdate" />
@can('update', $application)
<x-forms.button wire:click="getWildcardDomain">Generate Domain
@@ -226,20 +230,24 @@
id="customDockerRunOptions" label="Custom Docker Options" x-bind:disabled="!canUpdate" />
@else
@if ($application->could_set_build_commands())
@if ($buildPack === 'nixpacks')
@if ($buildPack === 'nixpacks' || $buildPack === 'railpack')
<div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input helper="If you modify this, you probably need to have a nixpacks.toml"
<x-forms.input helper="If you modify this, you probably need to have a {{ $buildPack === 'railpack' ? 'railpack.json' : 'nixpacks.toml' }}"
id="installCommand" label="Install Command" x-bind:disabled="!canUpdate" />
<x-forms.input helper="If you modify this, you probably need to have a nixpacks.toml"
<x-forms.input helper="If you modify this, you probably need to have a {{ $buildPack === 'railpack' ? 'railpack.json' : 'nixpacks.toml' }}"
id="buildCommand" label="Build Command" x-bind:disabled="!canUpdate" />
<x-forms.input helper="If you modify this, you probably need to have a nixpacks.toml"
<x-forms.input helper="If you modify this, you probably need to have a {{ $buildPack === 'railpack' ? 'railpack.json' : 'nixpacks.toml' }}"
id="startCommand" label="Start Command" x-bind:disabled="!canUpdate" />
</div>
<div class="pt-1 text-xs">Nixpacks will detect the required configuration
automatically.
@if ($buildPack === 'nixpacks')
<div class="pt-1 text-xs">
<span class="font-medium">Nixpacks</span>
will detect the required configuration automatically.
<a class="underline" href="https://coolify.io/docs/applications/">Framework
Specific Docs</a>
</div>
@endif
@endif
@endif
@@ -276,7 +284,7 @@
helper="It is calculated together with the Base Directory:<br><span class='dark:text-warning'>{{ Str::start($baseDirectory . $dockerComposeLocation, '/') }}</span>"
x-model="composeLocation" @blur="normalizeComposeLocation()" />
</div>
<div class="w-96">
<div class="w-full sm:w-96">
<x-forms.checkbox instantSave id="isPreserveRepositoryEnabled"
label="Preserve Repository During Deployment"
helper="Git repository (based on the base directory settings) will be copied to the deployment directory."
@@ -382,7 +390,7 @@
x-bind:disabled="!canUpdate" />
@if ($buildPack !== 'dockercompose')
<div class="pt-2 w-96">
<div class="pt-2 w-full sm:w-96">
<x-forms.checkbox
helper="Use a build server to build your application. You can configure your build server in the Server settings. For more info, check the <a href='https://coolify.io/docs/knowledge-base/server/build-server' class='underline' target='_blank'>documentation</a>."
instantSave id="isBuildServerEnabled" label="Use a Build Server?"
@@ -422,7 +430,7 @@
monacoEditorLanguage="yaml" useMonacoEditor />
</div>
@endif
<div class="w-96">
<div class="w-full sm:w-96">
<x-forms.checkbox label="Escape special characters in labels?"
helper="By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$.<br><br>If you want to use env variables inside the labels, turn this off."
id="isContainerLabelEscapeEnabled" instantSave
@@ -528,7 +536,7 @@
<h3 class="pt-8">HTTP Basic Authentication</h3>
<div>
<div class="w-96">
<div class="w-full sm:w-96">
<x-forms.checkbox helper="This will add the proper proxy labels to the container." instantSave
label="Enable" id="isHttpBasicAuthEnabled" x-bind:disabled="!canUpdate" />
</div>
@@ -550,7 +558,7 @@
<x-forms.textarea label="Container Labels" rows="15" id="customLabels"
monacoEditorLanguage="ini" useMonacoEditor x-bind:disabled="!canUpdate"></x-forms.textarea>
@endif
<div class="w-96">
<div class="w-full sm:w-96">
<x-forms.checkbox label="Readonly labels"
helper="Labels are readonly by default. Readonly means that edits you do to the labels could be lost and Coolify will autogenerate the labels for you. If you want to edit the labels directly, disable this option. <br><br>Be careful, it could break the proxy configuration after you restart the container as Coolify will now NOT autogenerate the labels for you (ofc you can always reset the labels to the coolify defaults manually)."
id="isContainerLabelReadonlyEnabled" instantSave
@@ -1,16 +1,17 @@
<nav wire:poll.10000ms="checkStatus" class="pb-6">
<x-resources.breadcrumbs :resource="$application" :parameters="$parameters" :title="$lastDeploymentInfo" :lastDeploymentLink="$lastDeploymentLink" />
<div class="navbar-main">
<nav class="flex shrink-0 gap-6 items-center whitespace-nowrap scrollbar min-h-10">
<a class="{{ request()->routeIs('project.application.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
<nav
class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-scroll overflow-y-hidden pb-1 whitespace-nowrap md:w-auto md:overflow-visible">
<a class="shrink-0 {{ request()->routeIs('project.application.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('project.application.configuration', $parameters) }}">
Configuration
</a>
<a class="{{ request()->routeIs('project.application.deployment.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
<a class="shrink-0 {{ request()->routeIs('project.application.deployment.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('project.application.deployment.index', $parameters) }}">
Deployments
</a>
<a class="{{ request()->routeIs('project.application.logs') ? 'dark:text-white' : '' }}"
<a class="shrink-0 {{ request()->routeIs('project.application.logs') ? 'dark:text-white' : '' }}"
href="{{ route('project.application.logs', $parameters) }}">
<div class="flex items-center gap-1">
Logs
@@ -23,100 +24,160 @@
</a>
@if (!$application->destination->server->isSwarm())
@can('canAccessTerminal')
<a class="{{ request()->routeIs('project.application.command') ? 'dark:text-white' : '' }}"
<a class="shrink-0 {{ request()->routeIs('project.application.command') ? 'dark:text-white' : '' }}"
href="{{ route('project.application.command', $parameters) }}">
Terminal
</a>
@endcan
@endif
<x-applications.links :application="$application" />
<div class="shrink-0">
<x-applications.links :application="$application" />
</div>
</nav>
<div class="flex flex-wrap gap-2 items-center">
@if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw))
<div>Please load a Compose file.</div>
@else
@if (!$application->destination->server->isSwarm())
<div>
<x-applications.advanced :application="$application" />
</div>
@endif
<div class="flex flex-wrap gap-2">
@if (!str($application->status)->startsWith('exited'))
@if (!$application->destination->server->isSwarm())
<x-forms.button title="With rolling update if possible" wire:click='deploy'>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-orange-400"
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M10.09 4.01l.496 -.495a2 2 0 0 1 2.828 0l7.071 7.07a2 2 0 0 1 0 2.83l-7.07 7.07a2 2 0 0 1 -2.83 0l-7.07 -7.07a2 2 0 0 1 0 -2.83l3.535 -3.535h-3.988">
</path>
<path d="M7.05 11.038v-3.988"></path>
</svg>
Redeploy
</x-forms.button>
<div class="md:hidden">
<x-dropdown>
<x-slot:title>
Actions
</x-slot>
@if (!str($application->status)->startsWith('exited'))
@if (!$application->destination->server->isSwarm())
<div class="dropdown-item dropdown-item-touch" wire:click='deploy'>
Redeploy
</div>
@endif
@if ($application->build_pack !== 'dockercompose')
@if ($application->destination->server->isSwarm())
<div class="dropdown-item dropdown-item-touch" wire:click='deploy'>
Update Service
</div>
@else
<div class="dropdown-item dropdown-item-touch" wire:click='restart'>
Restart
</div>
@endif
@endif
<x-modal-confirmation title="Confirm Application Stopping?" buttonTitle="Stop"
submitAction="stop" :checkboxes="$checkboxes" :actions="[
'This application will be stopped.',
'All non-persistent data of this application will be deleted.',
]" :confirmWithText="false" :confirmWithPassword="false"
step1ButtonText="Continue" step2ButtonText="Confirm">
<x-slot:trigger>
<div class="dropdown-item dropdown-item-touch text-error">
Stop
</div>
</x-slot:trigger>
</x-modal-confirmation>
@else
<div class="dropdown-item dropdown-item-touch" wire:click='deploy'>
Deploy
</div>
@endif
@if ($application->build_pack !== 'dockercompose')
@if ($application->destination->server->isSwarm())
<x-forms.button title="Redeploy Swarm Service (rolling update)" wire:click='deploy'>
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2">
<path
d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Update Service
</x-forms.button>
@if (!$application->destination->server->isSwarm())
<div class="mx-2 my-1 border-t border-neutral-200 dark:border-coolgray-300"></div>
@if ($application->status === 'running')
<div class="dropdown-item dropdown-item-touch" wire:click='force_deploy_without_cache'>
Force deploy (without cache)
</div>
@else
<x-forms.button title="Restart without rebuilding" wire:click='restart'>
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2">
<path
d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart
</x-forms.button>
<div class="dropdown-item dropdown-item-touch" wire:click='deploy(true)'>
Force deploy (without cache)
</div>
@endif
@endif
<x-modal-confirmation title="Confirm Application Stopping?" buttonTitle="Stop"
submitAction="stop" :checkboxes="$checkboxes" :actions="[
'This application will be stopped.',
'All non-persistent data of this application will be deleted.',
]" :confirmWithText="false" :confirmWithPassword="false"
step1ButtonText="Continue" step2ButtonText="Confirm">
<x-slot:button-title>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
<path
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
</svg>
Stop
</x-slot:button-title>
</x-modal-confirmation>
@else
<x-forms.button wire:click='deploy'>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
</x-forms.button>
</x-dropdown>
</div>
<div class="hidden flex-wrap items-center gap-2 md:flex">
@if (!$application->destination->server->isSwarm())
<div>
<x-applications.advanced :application="$application" />
</div>
@endif
<div class="flex flex-wrap gap-2">
@if (!str($application->status)->startsWith('exited'))
@if (!$application->destination->server->isSwarm())
<x-forms.button title="With rolling update if possible" wire:click='deploy'>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-orange-400"
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M10.09 4.01l.496 -.495a2 2 0 0 1 2.828 0l7.071 7.07a2 2 0 0 1 0 2.83l-7.07 7.07a2 2 0 0 1 -2.83 0l-7.07 -7.07a2 2 0 0 1 0 -2.83l3.535 -3.535h-3.988">
</path>
<path d="M7.05 11.038v-3.988"></path>
</svg>
Redeploy
</x-forms.button>
@endif
@if ($application->build_pack !== 'dockercompose')
@if ($application->destination->server->isSwarm())
<x-forms.button title="Redeploy Swarm Service (rolling update)" wire:click='deploy'>
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2">
<path
d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Update Service
</x-forms.button>
@else
<x-forms.button title="Restart without rebuilding" wire:click='restart'>
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2">
<path
d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart
</x-forms.button>
@endif
@endif
<x-modal-confirmation title="Confirm Application Stopping?" buttonTitle="Stop"
submitAction="stop" :checkboxes="$checkboxes" :actions="[
'This application will be stopped.',
'All non-persistent data of this application will be deleted.',
]" :confirmWithText="false" :confirmWithPassword="false"
step1ButtonText="Continue" step2ButtonText="Confirm">
<x-slot:button-title>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
<path
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
</svg>
Stop
</x-slot:button-title>
</x-modal-confirmation>
@else
<x-forms.button wire:click='deploy'>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
</x-forms.button>
@endif
</div>
</div>
@endif
</div>
@@ -68,6 +68,20 @@
@endif
</div>
</div>
@if ($application->build_pack === 'dockerimage')
<div class="flex flex-col gap-2 pt-4">
<h3>Manual Preview Deployment</h3>
<form wire:submit.prevent="addDockerImagePreview" class="flex flex-col gap-2 xl:flex-row xl:items-end">
<x-forms.input id="manualPullRequestId" label="Pull Request Id"
helper="Used as the preview identifier for naming, domains, logs, and cleanup." />
<x-forms.input id="manualDockerTag" label="Docker Tag"
helper="The image tag to deploy for this preview, for example pr_1234." />
@can('deploy', $application)
<x-forms.button type="submit">Deploy Preview</x-forms.button>
@endcan
</form>
</div>
@endif
@if ($application->previews->count() > 0)
<h3 class="py-4">Deployments</h3>
<div class="flex flex-wrap w-full gap-4">
@@ -87,11 +101,13 @@
<x-external-link />
</a>
@endif
|
<a target="_blank" href="{{ data_get($preview, 'pull_request_html_url') }}">Open
PR on Git
<x-external-link />
</a>
@if (filled(data_get($preview, 'pull_request_html_url')))
|
<a target="_blank" href="{{ data_get($preview, 'pull_request_html_url') }}">Open
PR on Git
<x-external-link />
</a>
@endif
@if (count($parameters) > 0)
|
<a {{ wireNavigate() }}
@@ -131,6 +147,10 @@
<form wire:submit="save_preview('{{ $preview->id }}')" class="flex items-end gap-2 pt-4">
<x-forms.input label="Domain" helper="One domain per preview."
id="previewFqdns.{{ $previewName }}" canGate="update" :canResource="$application"></x-forms.input>
@if ($application->build_pack === 'dockerimage')
<x-forms.input label="Docker Tag" helper="The image tag used for this preview deployment."
id="previewDockerTags.{{ $previewName }}" canGate="update" :canResource="$application"></x-forms.input>
@endif
@can('update', $application)
<x-forms.button type="submit">Save</x-forms.button>
<x-forms.button wire:click="generate_preview('{{ $preview->id }}')">Generate
@@ -157,7 +177,8 @@
Force deploy (without
cache)
</x-forms.button>
<x-forms.button wire:click="deploy({{ data_get($preview, 'pull_request_id') }})">
<x-forms.button
wire:click="deploy({{ data_get($preview, 'pull_request_id') }}, null, false, '{{ data_get($preview, 'docker_registry_image_tag') }}')">
@if (data_get($preview, 'status') === 'exited')
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none"
@@ -0,0 +1,17 @@
<span>
@if ($application->server_status === false)
<span title="One or more servers are unreachable or misconfigured.">
<svg class="w-4 h-4 text-error" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16" />
</svg>
</span>
@elseif ($application->additional_servers()->exists() && str($application->status)->contains('degraded'))
<span title="Application is in degraded state across multiple servers.">
<svg class="w-4 h-4 text-error" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16" />
</svg>
</span>
@endif
</span>
@@ -2,6 +2,7 @@
<form wire:submit='submit' class="flex flex-col">
<div class="flex items-center gap-2">
<h2>Swarm Configuration</h2>
<x-deprecated-badge />
@can('update', $application)
<x-forms.button type="submit">
Save
@@ -13,6 +14,9 @@
</x-forms.button>
@endcan
</div>
<x-callout type="warning" title="Deprecated" class="my-4">
{{ config('deprecations.swarm') }}
</x-callout>
<div class="flex flex-col gap-2 py-4">
<div class="flex flex-col items-end gap-2 xl:flex-row">
<x-forms.input id="swarmReplicas" label="Replicas" required canGate="update" :canResource="$application" />