2026-08-03 21:11:54 +00:00
@ php
$configuredCount = collect ( $domainRows ) -> where ( 'is_suggested' , false ) -> count ();
$suggestedCount = collect ( $domainRows ) -> where ( 'is_suggested' , true ) -> count ();
$hasRows = count ( $domainRows ) > 0 ;
2026-08-21 19:22:04 +00:00
$hasDnsChecksInProgress = collect ( $domainRows ) -> contains ( fn ( $row ) => $row [ 'dns_status' ] === 'checking' );
2026-08-06 16:15:58 +00:00
$composeDomainGroups = collect ( $domainRows )
-> groupBy ( fn ( $row ) => $row [ 'service' ] ? ? '__unknown' )
-> filter ( fn ( $rows ) => $rows -> contains ( fn ( $row ) => ! ( $row [ 'is_suggested' ] ? ? false )));
2026-08-18 07:08:18 +00:00
$hasHttpsDomains = collect ( $domainRows ) -> contains (
fn ( $row ) => ! ( $row [ 'is_suggested' ] ? ? false ) && str_starts_with ( strtolower ( $row [ 'url' ]), 'https://' )
);
2026-08-03 21:11:54 +00:00
@ endphp
2026-09-07 16:29:58 +00:00
< div id = " application-domains-section " class = " domains-overview-container flex flex-col gap-4 "
2026-08-05 11:50:11 +00:00
x - data = " {
2026-08-06 16:15:58 +00:00
domainSearch : '' ,
2026-08-05 11:50:11 +00:00
modalOpen : @ js ( $showEditDomainModal || $editDomainDnsFailed ),
editingServiceLabel : @ js ( $editingService ? ? '' ),
2026-09-10 12:00:50 +00:00
openEditDomain ( index , domain , parts , service , indexing , redirect ) {
if ( index !== undefined ) {
$wire . set ( 'editingIndex' , index , false );
$wire . set ( 'editingDomain' , domain , false );
$wire . set ( 'editingDomainParts' , parts , false );
$wire . set ( 'editingDomainPartsChanged' , false , false );
$wire . set ( 'editingService' , service , false );
$wire . set ( 'editingIndexing' , indexing , false );
$wire . set ( 'editingRedirect' , redirect , false );
$wire . set ( 'editingOriginalRedirect' , redirect , false );
$wire . set ( 'editingDomainWasRegenerated' , false , false );
$wire . set ( 'editingGeneratedHost' , null , false );
}
this . editingServiceLabel = service ? ? $wire . editingService ? ? '' ;
2026-08-05 11:50:11 +00:00
this . modalOpen = true ;
2026-09-07 16:29:58 +00:00
this . $nextTick (() => document . getElementById ( 'editingDomainParts-host' ) ? . focus ? . ());
2026-08-05 11:50:11 +00:00
},
2026-09-10 12:00:50 +00:00
closeEditDomain ( discardDraft = true ) {
2026-08-05 11:50:11 +00:00
this . modalOpen = false ;
this . editingServiceLabel = '' ;
2026-09-10 12:00:50 +00:00
if ( discardDraft ) this . $wire . cancelEdit ();
2026-08-05 11:50:11 +00:00
},
2026-08-06 16:15:58 +00:00
matchesDomainSearch ( value ) {
return ! this . domainSearch . trim () || value . toLowerCase () . includes ( this . domainSearch . trim () . toLowerCase ());
},
hasDomainSearchResults ( values ) {
return values . some (( value ) => this . matchesDomainSearch ( value ));
},
2026-08-05 11:50:11 +00:00
} "
2026-08-17 14:05:21 +00:00
@ open - edit - domain . window = " openEditDomain() "
2026-09-10 12:00:50 +00:00
@ edit - domain - saved . window = " closeEditDomain(false) " >
2026-08-21 19:22:04 +00:00
@ if ( $hasDnsChecksInProgress )
< div class = " hidden " wire : poll . 2000 ms = " pollDnsChecks " aria - hidden = " true " ></ div >
@ endif
2026-09-07 16:29:58 +00:00
@ if ( $labelsAreWritable )
< x - callout type = " warning " title = " Domains managed via labels " class = " mb-4 " >
Container label readonly mode is disabled . Domains must be set in the Labels section on the General page .
</ x - callout >
@ endif
2026-08-12 21:52:27 +00:00
2026-09-07 16:29:58 +00:00
@ if ( $isCompose && count ( $composeServices ) === 0 )
< x - callout type = " info " title = " No services " >
No non - database services found in the Docker Compose file . Domains can only be assigned to application
services .
</ x - callout >
@ endif
2026-08-18 07:08:18 +00:00
2026-09-07 16:29:58 +00:00
@ cannot ( 'update' , $application )
< x - callout type = " danger " title = " Insufficient permissions " >
You don ' t have permission to manage domains . Contact your team administrator for access .
</ x - callout >
@ endcannot
2026-08-03 21:11:54 +00:00
{{ -- Toolbar -- }}
2026-09-07 16:29:58 +00:00
< div class = " flex flex-wrap items-center gap-2 " >
2026-08-03 21:11:54 +00:00
< div class = " min-w-0 flex-1 " >
2026-09-07 16:29:58 +00:00
< h2 id = " domains-section " > Domains </ h2 >
2026-08-03 21:11:54 +00:00
< p class = " text-[13px] text-neutral-500 dark:text-fg-dim " >
{{ $configuredCount }} domain {{ $configuredCount === 1 ? '' : 's' }}
@ if ( $suggestedCount > 0 )
2026-08-05 11:50:11 +00:00
· {{ $suggestedCount }} not added
2026-08-03 21:11:54 +00:00
@ endif
</ p >
</ div >
< div class = " ml-auto flex flex-wrap items-center gap-2 " >
2026-09-07 16:29:58 +00:00
@ if ( $hasRows )
2026-08-06 16:15:58 +00:00
< div class = " relative w-full sm:w-64 " >
< x - reicon name = " search "
class = " pointer-events-none absolute top-1/2 left-2.5 z-10 size-3.5 -translate-y-1/2 text-neutral-400 dark:text-fg-faint " />
< input type = " search " x - model = " domainSearch " aria - label = " Search services or domains "
class = " input h-8! w-full pl-8! text-[13px]! " placeholder = " Search services or domains " />
</ div >
@ endif
2026-08-03 21:11:54 +00:00
@ can ( 'update' , $application )
2026-09-10 12:00:50 +00:00
< x - forms . button wire : click = " checkAllDns " : showLoadingIndicator = " false " wire : loading . attr = " disabled " wire : target = " checkAllDns,checkDomainDns " >
2026-09-07 16:29:58 +00:00
< x - reicon name = " refresh " class = " size-3.5 " />
Check all DNS
</ x - forms . button >
2026-08-10 13:10:30 +00:00
< div class = " relative shrink-0 " >
@ include ( 'livewire.project.shared.cloudflare-autoconfigure' )
</ div >
2026-08-03 21:11:54 +00:00
@ unless ( $labelsAreWritable )
@ if ( ! $isCompose || count ( $composeServices ) > 0 )
< x - modal - input title = " Add domain " : closeOutside = " false " : wireIgnore = " false "
canGate = " update " : canResource = " $application " >
< x - slot : content >
< button type = " button "
2026-08-05 20:34:56 +00:00
class = " button button-highlighted " >
2026-08-03 21:11:54 +00:00
< x - reicon name = " plus " class = " size-3.5 " />
2026-09-07 16:29:58 +00:00
Add domain
2026-08-03 21:11:54 +00:00
</ button >
</ x - slot : content >
< form wire : submit = " addDomain " class = " application-settings-form flex flex-col gap-4 " >
@ if ( $isCompose && count ( $composeServices ) > 0 )
2026-08-18 08:18:52 +00:00
< x - forms . listbox canGate = " update " : canResource = " $application " label = " Service " id = " newDomainService " required
2026-08-10 22:06:55 +00:00
: options = " collect( $composeServices )->map(fn ( $serviceName ) => [
'value' => $serviceName ,
'label' => $serviceName ,
]) -> values () -> all () "
: disabled = " ! auth()->user()->can('update', $application ) " />
2026-08-03 21:11:54 +00:00
@ endif
2026-08-17 14:05:21 +00:00
< x - forms . domain - input id = " newDomainParts " errorId = " newDomain " />
2026-08-03 21:11:54 +00:00
@ if ( $addDomainDnsFailed )
2026-08-05 11:50:11 +00:00
< x - callout type = " danger " title = " DNS is not pointing to the right IP " >
This domain does not currently resolve to this server .
Traffic may not reach Coolify until you update DNS .
Are you sure you want to add it anyway ?
@ if ( filled ( $addDomainDnsMessage ))
< div class = " pt-2 " > {{ $addDomainDnsMessage }} </ div >
2026-08-03 21:11:54 +00:00
@ endif
</ x - callout >
@ endif
< div class = " flex flex-wrap items-center justify-between gap-2 pt-2 " >
< x - forms . button type = " button " wire : click = " generateDomain " >
Generate domain
</ x - forms . button >
< div class = " flex flex-wrap gap-2 " >
@ if ( $addDomainDnsFailed )
< x - forms . button type = " button " wire : click = " confirmAddDomainDespiteDns " isError >
Continue
</ x - forms . button >
@ else
< x - forms . button type = " submit " isHighlighted >
Save
</ x - forms . button >
@ endif
</ div >
</ div >
</ form >
</ x - modal - input >
@ endif
@ endunless
@ endcan
</ div >
</ div >
2026-09-07 16:29:58 +00:00
@ if ( $hasHttpsDomains && ! $labelsAreWritable )
< div class = " flex flex-wrap items-center justify-end gap-2 service-domains-https " >
< label for = " isForceHttpsEnabled-trigger " class = " mb-0! text-[12px]! " > Redirect HTTP to HTTPS </ label >
< x - helper helper = " Disable only when Cloudflare Tunnel or another proxy connects to Coolify over HTTP. Keep enabled when Cloudflare uses Full or Full (Strict) SSL. " />
< div class = " w-28 shrink-0 " >
< x - forms . listbox canGate = " update " : canResource = " $application " id = " isForceHttpsEnabled "
onChange = " updateForceHttps " portal
: options = " [
[ 'value' => true , 'label' => 'Enabled' ],
[ 'value' => false , 'label' => 'Disabled' ],
] " :disabled= " ! auth () -> user () -> can ( 'update' , $application ) " />
</ div >
</ div >
@ endif
2026-08-03 21:11:54 +00:00
{{ -- Table / empty -- }}
< div id = " domains-table-section "
class = " application-settings-section-body mt-1 scroll-mt-28 { { $hasRows ? 'is-flush' : '' }} w-full " >
2026-09-07 16:29:58 +00:00
@ if ( $hasRows )
< div class = " data-table-header service-domains-overview-grid " >
< span > Domain </ span >
< span > Protocol redirect </ span >
< span > Domain redirect </ span >
< span > Internal port </ span >
< span > Search indexing </ span >
< span > DNS status </ span >
< span class = " text-right " > Actions </ span >
</ div >
@ endif
2026-08-03 21:11:54 +00:00
@ if ( $isCompose && count ( $composeServices ) === 0 && ! $hasRows )
< x - empty size = " sm " title = " No services available "
description = " No non-database services found in the Docker Compose file. "
icon - name = " globe " />
2026-08-06 16:15:58 +00:00
@ elseif ( $isCompose && $composeDomainGroups -> isEmpty ())
< x - empty size = " sm " title = " No domains configured "
2026-09-07 16:29:58 +00:00
description = " Add your first domain with the Add domain button above. Choose which service receives it. "
2026-08-06 16:15:58 +00:00
icon - name = " globe " />
2026-08-03 21:11:54 +00:00
@ elseif ( ! $hasRows )
< x - empty size = " sm " title = " No domains configured "
2026-09-07 16:29:58 +00:00
description = " Add your first domain with the Add domain button above, or generate one with the server wildcard domain. "
2026-08-03 21:11:54 +00:00
icon - name = " globe " />
@ elseif ( $isCompose )
@ php
2026-08-06 16:15:58 +00:00
$grouped = $composeDomainGroups ;
$serviceOrder = collect ( $composeServices )
-> filter ( fn ( $serviceName ) => $grouped -> has ( $serviceName ))
-> values ()
-> all ();
2026-08-03 21:11:54 +00:00
foreach ( $grouped -> keys () as $name ) {
if ( $name !== '__unknown' && ! in_array ( $name , $serviceOrder , true )) {
$serviceOrder [] = $name ;
}
}
2026-08-06 16:15:58 +00:00
$domainSearchValues = collect ( $serviceOrder )
-> map ( fn ( $serviceName ) => $serviceName . ' ' . $grouped -> get ( $serviceName , collect ()) -> pluck ( 'url' ) -> implode ( ' ' ))
-> values ();
2026-08-03 21:11:54 +00:00
@ endphp
2026-08-10 13:15:48 +00:00
< div >
2026-08-03 21:11:54 +00:00
@ foreach ( $serviceOrder as $serviceName )
@ php
$rows = $grouped -> get ( $serviceName , collect ());
$redirectWireKey = $this -> serviceRedirectWireKey ( $serviceName );
@ endphp
2026-08-06 16:15:58 +00:00
< section id = " application-compose-domain-group- { { $redirectWireKey }} "
wire : key = " application-compose-domain-group- { { $redirectWireKey }} "
x - show = " matchesDomainSearch(@js( $serviceName .' '. $rows->pluck ('url')->implode(' '))) "
class = " border-b border-neutral-200 last:border-b-0 dark:border-white/10 " >
2026-08-11 09:28:46 +00:00
< div class = " flex w-full items-center justify-between gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-3 dark:border-white/10 dark:bg-white/[0.04] " >
2026-08-06 16:15:58 +00:00
< span class = " min-w-0 flex-1 truncate text-sm font-medium text-black dark:text-white " >
{{ $serviceName }}
</ span >
2026-08-03 21:11:54 +00:00
</ div >
2026-09-07 16:29:58 +00:00
< div wire : key = " application-compose-domain-rows- { { $redirectWireKey }} "
2026-08-06 16:15:58 +00:00
class = " data-table w-full " >
@ foreach ( $rows as $row )
@ php
$index = collect ( $domainRows ) -> search (
fn ( $item ) => $item [ 'url' ] === $row [ 'url' ]
&& ( $item [ 'service' ] ? ? null ) === ( $row [ 'service' ] ? ? null )
&& ( bool ) ( $item [ 'is_suggested' ] ? ? false ) === ( bool ) ( $row [ 'is_suggested' ] ? ? false ),
);
@ endphp
@ include ( 'livewire.project.application.partials.domain-row' , [
'index' => $index ,
'row' => $row ,
'application' => $application ,
'labelsAreWritable' => $labelsAreWritable ,
2026-09-07 16:29:58 +00:00
'isCompose' => true ,
2026-08-06 16:15:58 +00:00
])
@ endforeach
</ div >
</ section >
2026-08-03 21:11:54 +00:00
@ endforeach
2026-08-06 16:15:58 +00:00
< div x - cloak
x - show = " domainSearch.trim() && !hasDomainSearchResults(@js( $domainSearchValues )) "
class = " px-4 py-8 " >
< x - empty size = " sm " title = " No domains found "
description = " No service or domain matches your search. " icon - name = " search " />
</ div >
2026-08-03 21:11:54 +00:00
</ div >
@ else
< div class = " data-table w-full " >
@ foreach ( $domainRows as $index => $row )
@ include ( 'livewire.project.application.partials.domain-row' , [
'index' => $index ,
'row' => $row ,
'application' => $application ,
'labelsAreWritable' => $labelsAreWritable ,
'isCompose' => false ,
])
@ endforeach
</ div >
2026-09-07 16:29:58 +00:00
< div x - cloak x - show = " domainSearch.trim() && !hasDomainSearchResults(@js(collect( $domainRows )->pluck('url')->values())) "
class = " px-4 py-8 " >
< x - empty size = " sm " title = " No domains found "
description = " No domain matches your search. " icon - name = " search " />
</ div >
2026-08-03 21:11:54 +00:00
@ endif
</ div >
2026-09-07 16:29:58 +00:00
{{ -- One dialog for address edits and automatically saved domain settings . -- }}
2026-08-05 11:50:11 +00:00
< div class = " relative h-auto w-auto " : class = " { 'z-40': modalOpen } "
@ keydown . window . escape = " if (modalOpen) { closeEditDomain() } " >
< template x - teleport = " body " >
< div x - show = " modalOpen " class = " fixed inset-0 z-99 overflow-y-auto " x - cloak >
< div x - show = " modalOpen " x - transition : enter = " ease-out duration-100 "
x - transition : enter - start = " opacity-0 " x - transition : enter - end = " opacity-100 "
x - transition : leave = " ease-in duration-100 " x - transition : leave - start = " opacity-100 "
x - transition : leave - end = " opacity-0 "
class = " absolute inset-0 h-full w-full bg-black/50 backdrop-blur-[2px] "
@ click = " closeEditDomain() " ></ div >
< div class = " relative flex min-h-full items-start justify-center p-4 sm:items-center " >
< div x - show = " modalOpen " x - trap . inert . noscroll = " modalOpen "
x - transition : enter = " ease-out duration-100 "
x - transition : enter - start = " opacity-0 -translate-y-2 sm:scale-95 "
x - transition : enter - end = " opacity-100 translate-y-0 sm:scale-100 "
x - transition : leave = " ease-in duration-100 "
x - transition : leave - start = " opacity-100 translate-y-0 sm:scale-100 "
x - transition : leave - end = " opacity-0 -translate-y-2 sm:scale-95 "
class = " application-settings-form application-settings-section relative flex max-h-[calc(100dvh-2rem)] w-full flex-col overflow-hidden lg:w-auto lg:min-w-2xl lg:max-w-4xl "
style = " box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-modal) " >
< header class = " flex-nowrap! " >
2026-09-07 16:29:58 +00:00
< h3 class = " min-w-0 flex-1 truncate " > Domain settings </ h3 >
2026-08-05 11:50:11 +00:00
< button type = " button " @ click = " closeEditDomain() "
class = " icon-button shrink-0 " aria - label = " Close " >
< x - reicon name = " x " class = " size-4 " />
</ button >
</ header >
2026-09-08 20:21:32 +00:00
< div class = " application-settings-section-body relative flex min-h-0 flex-1 flex-col overflow-hidden " >
< form wire : submit = " updateDomain " class = " flex min-h-0 flex-1 flex-col " >
< div data - testid = " domain-settings-scroll "
class = " flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto overscroll-contain pb-4 "
style = " -webkit-overflow-scrolling: touch; " >
2026-08-05 11:50:11 +00:00
< div x - show = " editingServiceLabel " x - cloak class = " w-full " >
< div class = " mb-1.5 flex h-4 w-full items-center gap-1.5 " >
< label class = " mb-0! flex items-center gap-1 text-sm font-medium leading-4 " > Service </ label >
</ div >
< input type = " text " class = " input " readonly x - bind : value = " editingServiceLabel " />
</ div >
2026-08-03 21:11:54 +00:00
2026-08-17 14:05:21 +00:00
< x - forms . domain - input id = " editingDomainParts " errorId = " editingDomain " />
2026-08-10 13:10:30 +00:00
2026-08-05 11:50:11 +00:00
@ if ( $editDomainDnsFailed )
< x - callout type = " danger " title = " DNS is not pointing to the right IP " >
This domain does not currently resolve to this server .
Traffic may not reach Coolify until you update DNS .
Are you sure you want to save it anyway ?
@ if ( filled ( $editDomainDnsMessage ))
< div class = " pt-2 " > {{ $editDomainDnsMessage }} </ div >
@ endif
</ x - callout >
@ endif
2026-08-03 21:11:54 +00:00
2026-09-10 12:00:50 +00:00
@ unless ( $labelsAreWritable )
2026-09-08 20:21:32 +00:00
@ can ( 'update' , $application )
2026-09-10 12:00:50 +00:00
< div
2026-09-08 20:21:32 +00:00
class = " grid grid-cols-1 gap-4 border-t border-neutral-200 pt-4 sm:grid-cols-2 dark:border-white/10 " >
2026-09-10 12:00:50 +00:00
< x - forms . listbox id = " editingIndexing "
htmlId = " application-domain-indexing " label = " Search engine indexing " portal
2026-09-07 16:29:58 +00:00
: options = " [
[ 'value' => 'index' , 'label' => 'Indexable' ],
[ 'value' => 'noindex' , 'label' => 'Noindex' ],
] " />
2026-09-10 12:00:50 +00:00
< x - forms . listbox id = " editingRedirect "
htmlId = " application-domain-direction " label = " www redirect "
2026-09-07 16:29:58 +00:00
: helper = " $isCompose ? 'Applies to all domains for this Compose service.' : 'Applies to all domains for this application.' "
2026-09-10 12:00:50 +00:00
portal
2026-09-07 16:29:58 +00:00
: options = " [
[ 'value' => 'both' , 'label' => 'No redirect' ],
[ 'value' => 'www' , 'label' => 'Redirect to www' ],
[ 'value' => 'non-www' , 'label' => 'Redirect to non-www' ],
] " />
2026-09-08 20:21:32 +00:00
</ div >
@ endcan
2026-09-10 12:00:50 +00:00
@ endunless
2026-09-08 20:21:32 +00:00
</ div >
2026-09-07 16:29:58 +00:00
2026-09-08 20:21:32 +00:00
< div data - testid = " domain-settings-footer "
class = " shrink-0 border-t border-neutral-200 pt-4 dark:border-white/10 " >
2026-09-10 12:00:50 +00:00
< div class = " flex flex-wrap items-center justify-between gap-2 " >
< x - forms . button type = " button " wire : click = " regenerateEditingDomain "
wire : target = " regenerateEditingDomain " >
Regenerate hostname
</ x - forms . button >
2026-09-08 20:21:32 +00:00
@ if ( $editDomainDnsFailed )
< x - forms . button type = " button " isError wire : click = " confirmUpdateDomainDespiteDns " >
Continue
</ x - forms . button >
@ else
< x - forms . button type = " submit " wire : target = " updateDomain " isHighlighted >
Save
</ x - forms . button >
@ endif
</ div >
</ div >
</ form >
2026-08-03 21:11:54 +00:00
</ div >
</ div >
</ div >
2026-08-05 11:50:11 +00:00
</ div >
</ template >
</ div >
2026-08-03 21:11:54 +00:00
< x - domain - conflict - modal : conflicts = " $domainConflicts " : showModal = " $showDomainConflictModal "
confirmAction = " confirmDomainUsage " />
2026-09-02 17:39:19 +00:00
@ if ( $showPortWarningModal )
< div x - data = " { modalOpen: true } "
@ keydown . escape . window = " modalOpen = false; $wire .call('cancelUseUnknownPort') "
class = " relative z-40 " >
< template x - teleport = " body " >
< div x - show = " modalOpen "
class = " fixed inset-0 z-99 flex min-h-full items-center justify-center overflow-y-auto p-4 " x - cloak >
< div class = " absolute inset-0 bg-black/50 backdrop-blur-[2px] " ></ div >
< div x - show = " modalOpen " x - trap . inert . noscroll = " modalOpen "
class = " application-settings-form application-settings-section relative w-full lg:min-w-[36rem] lg:max-w-2xl "
style = " box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-modal) " >
< header >
< h3 > Use a different port ? </ h3 >
< button type = " button "
@ click = " modalOpen = false; $wire .call('cancelUseUnknownPort') "
class = " icon-button " aria - label = " Close " >
< x - reicon name = " x " class = " size-4 " />
</ button >
</ header >
< div class = " application-settings-section-body " >
< x - callout type = " warning " title = " Unrecognized internal port " class = " mb-4 " >
Port < strong > {{ $unrecognizedPort }} </ strong > is not listed in Ports Exposes
and is not used by any application domain . The proxy will still route to it ,
but the container may not be listening there .
</ x - callout >
< div class = " mt-4 flex flex-wrap justify-end gap-2 border-t border-neutral-200 pt-4 dark:border-white/[0.08] " >
< x - forms . button type = " button " canGate = " update " : canResource = " $application "
@ click = " modalOpen = false; $wire .call('cancelUseUnknownPort') " >
Cancel
</ x - forms . button >
< x - forms . button type = " button " wire : click = " confirmUseUnknownPort " canGate = " update "
: canResource = " $application "
@ click = " modalOpen = false " isError >
Use this port anyway
</ x - forms . button >
</ div >
</ div >
</ div >
</ div >
</ template >
</ div >
@ endif
2026-08-03 21:11:54 +00:00
</ div >