{ if (o) focusFirstInput(); else onClose(); }}> {#if !isEditing} Add environment {:else} Edit environment {/if} {#if environment} {environment.name} {/if} {#if formError}
{formError}
{/if} General Updates Activity Security Notifications
formIcon = icon} /> formErrors.name = undefined} />
{#if formErrors.name}

{formErrors.name}

{/if}
({formLabels.length}/{MAX_LABELS})
{#if formLabels.length > 0}
{#each formLabels as label} {label} {/each}
{/if} {#if formLabels.length < MAX_LABELS}
showLabelDropdown = true} onblur={() => setTimeout(() => showLabelDropdown = false, 150)} onkeydown={(e) => { if (e.key === 'Enter') { e.preventDefault(); const trimmed = newLabelInput.trim().toLowerCase(); if (trimmed && !formLabels.includes(trimmed)) { formLabels = [...formLabels, trimmed]; newLabelInput = ''; } } else if (e.key === 'Escape') { showLabelDropdown = false; } }} /> {#if showLabelDropdown && filteredLabelSuggestions.length > 0}
{#each filteredLabelSuggestions as suggestion} {@const colors = { color: getLabelColor(suggestion), bgColor: getLabelBgColor(suggestion) }} {/each}
{/if}
{:else}

Maximum labels reached

{/if}

Unix socket

Connect via Docker socket on the same machine. Default path: /var/run/docker.sock. Also works with Docker Desktop and OrbStack.

Direct connection

Connect directly to Docker Engine API. Requires Docker to expose its API on a TCP port (default 2375/2376). Best for LAN environments.

Hawser standard

Hawser agent listens on a port and Dockhand connects to it. Good for LAN with static IPs.

Hawser edge

Hawser agent initiates outbound WebSocket to Dockhand. No port forwarding needed. Perfect for VPS, NAT, or dynamic IPs.

Learn more about Hawser
{ formConnectionType = v as ConnectionType; // Set default port based on connection type if (v === 'direct') { formPort = 2375; } else if (v === 'hawser-standard') { formPort = 2376; } }}> {#if formConnectionType === 'socket'} Unix socket {:else if formConnectionType === 'direct'} Direct connection {:else if formConnectionType === 'hawser-standard'} Hawser agent (standard) {:else} Hawser agent (edge) {/if} Unix socket Direct connection Hawser agent (standard) Hawser agent (edge)

{#if formConnectionType === 'socket'} Connect via Unix socket on the same machine. {:else if formConnectionType === 'direct'} Connect directly to Docker Engine API on TCP port. {:else if formConnectionType === 'hawser-standard'} Hawser agent listens, Dockhand connects. {:else} Hawser agent connects out to Dockhand. No port forwarding needed. {/if}

{#if formConnectionType === 'socket'}
{#if showSocketDropdown && detectedSockets.length > 1}
{#each detectedSockets as socket} {/each}
{/if}

Click to auto-detect available Docker sockets

{/if} {#if formConnectionType === 'direct'}
{ formErrors.host = undefined; handleHostInput(); }} onblur={() => handleHostInput(true)} /> {#if formErrors.host}

{formErrors.host}

{/if}
formProtocol = v}> {#if formProtocol === 'https'} HTTPS (TLS) {:else} HTTP {/if} HTTP HTTPS (TLS)
{#if formProtocol === 'https'}

TLS certificates for mTLS authentication (RSA or ECDSA)

{/if} {/if} {#if formConnectionType === 'hawser-standard'}
{ formErrors.host = undefined; handleHostInput(); }} onblur={() => handleHostInput(true)} /> {#if formErrors.host}

{formErrors.host}

{/if}
formProtocol = v}> {#if formProtocol === 'https'} HTTPS (TLS) {:else} HTTP {/if} HTTP HTTPS (TLS)
{#if formProtocol === 'https'}

Paste the CA certificate if agent uses self-signed TLS (RSA or ECDSA).

Disable certificate validation (insecure)

{/if}

If the Hawser agent is configured with TOKEN, enter it here.

Run Hawser agent on the target host: hawser --port {formPort}
{/if} {#if formConnectionType === 'hawser-edge'}
{#if isEditing && environment}
{#if environment.hawserAgentId} Connected {:else} Waiting for agent {/if}
{#if environment.hawserAgentId}

Agent: {environment.hawserAgentName || environment.hawserAgentId}

{#if environment.hawserVersion}

Version: {environment.hawserVersion}

{/if} {#if environment.hawserLastSeen}

Last seen: {new Date(environment.hawserLastSeen).toLocaleString()}

{/if}
{/if} {/if}
{#if isEditing && hawserToken} {:else if isEditing && !hawserToken && !hawserTokenLoading} {/if}
{#if !isEditing} {#if !pendingToken}

Generate a token now. It will be saved when you add the environment.

{:else}

Copy this token now - you'll need it for the Hawser agent!

Run on your host:
DOCKHAND_SERVER_URL={getConnectionUrl()} TOKEN={pendingToken} hawser
{/if} {/if} {#if isEditing} {#if hawserTokenLoading}
{:else if generatedToken}

Save this token now - it won't be shown again!

Run on your host:
DOCKHAND_SERVER_URL={getConnectionUrl()} TOKEN={generatedToken} hawser
{:else if hawserToken}
{hawserToken.tokenPrefix}... {#if hawserToken.lastUsed} Last used: {new Date(hawserToken.lastUsed).toLocaleDateString()} {/if}
{:else}

No token generated yet. Click Generate above.

{/if} {/if}
{/if} {#if formConnectionType !== 'hawser-edge'}

IP address or hostname where container ports are accessible from your browser. For local Docker, use the server's LAN IP.

Used for clickable port links on the containers page

{/if}
Scheduled update check

Periodically check all containers in this environment for available image updates.

{#if updateCheckLoading}
{:else}

Automatically check for container updates on a schedule

{#if updateCheckEnabled}
updateCheckCron = cron} />

When enabled, containers will be updated automatically when new images are found. When disabled, only sends notifications about available updates.

{#if updateCheckAutoUpdate && scannerEnabled}

Block auto-updates if the new image has vulnerabilities exceeding this criteria

{/if}
{#if updateCheckAutoUpdate} {#if scannerEnabled && updateCheckVulnerabilityCriteria !== 'never'} New images are pulled to a temporary tag, scanned, then deployed if they pass the vulnerability check. Blocked images are deleted automatically. {:else} Containers will be updated automatically when new images are available. {/if} {:else} You'll receive notifications when updates are available. Containers won't be modified. {/if}
{/if} {/if}

Used for scheduling auto-updates and git syncs

Track container events (start, stop, restart, etc.) from this environment in real-time

Collect CPU and memory usage statistics from this environment

Show amber glow when container values change in the containers list

Vulnerability scanning
{#if !isEditing}

Scan images for known security vulnerabilities

{#if formEnableScanner}

Choose vulnerability scanner

{ formScannerType = v as ScannerType; }} />
Scanner images will be pulled automatically on first scan. Vulnerability databases are cached in Docker volumes for faster subsequent scans.
{/if} {:else if scannerLoading}
{:else}

Scan images for known security vulnerabilities

{#if scannerEnabled}

Choose vulnerability scanner

{ selectedScanner = v as ScannerType; }} />
{#if selectedScanner === 'grype' || selectedScanner === 'both'}
Grype {#if loadingScannerVersions} {:else if scannerAvailability.grype && scannerVersions.grype} v{scannerVersions.grype} {:else if scannerAvailability.grype} Ready {:else} Not installed {/if} {#if !loadingScannerVersions} {#if !scannerAvailability.grype} loadScannerSettings(environment?.id)}> {:else} {#if grypeUpdateStatus === 'up-to-date'} Latest {:else if grypeUpdateStatus === 'update-available' || pullingGrype} {:else} {/if} {/if} {/if}
{/if} {#if selectedScanner === 'trivy' || selectedScanner === 'both'}
Trivy {#if loadingScannerVersions} {:else if scannerAvailability.trivy && scannerVersions.trivy} v{scannerVersions.trivy} {:else if scannerAvailability.trivy} Ready {:else} Not installed {/if} {#if !loadingScannerVersions} {#if !scannerAvailability.trivy} loadScannerSettings(environment?.id)}> {:else} {#if trivyUpdateStatus === 'up-to-date'} Latest {:else if trivyUpdateStatus === 'update-available' || pullingTrivy} {:else} {/if} {/if} {/if}
{/if} {#if ((selectedScanner === 'grype' || selectedScanner === 'both') && !scannerAvailability.grype) || ((selectedScanner === 'trivy' || selectedScanner === 'both') && !scannerAvailability.trivy)}
Scanner images will be pulled automatically on first scan. Vulnerability databases are cached in Docker volumes for faster subsequent scans.
{/if}
{/if} {/if}
Notification channels
{#if !isEditing}

Select which notification channels should send alerts for events from this environment.

{#if notifications.length === 0}

No notification channels configured yet.

Create notification channels in the Notifications settings tab first.

{:else}
{#each notifications as channel (channel.id)} {@const isSelected = formSelectedNotifications.some(n => n.id === channel.id)} {@const selectedNotif = formSelectedNotifications.find(n => n.id === channel.id)}
{#if channel.type === 'smtp'} {:else} {/if} {channel.name} ({channel.type})
{ if (isSelected) { formSelectedNotifications = formSelectedNotifications.filter(n => n.id !== channel.id); } else { formSelectedNotifications = [...formSelectedNotifications, { id: channel.id, eventTypes: NOTIFICATION_EVENT_TYPES.map(e => e.id) }]; } }} />
{#if !channel.enabled}

Channel disabled globally

{/if} {#if isSelected && selectedNotif} {@const isCollapsed = collapsedChannels.has(channel.id)}
toggleChannelCollapse(channel.id)} onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleChannelCollapse(channel.id); } }} > {#if isCollapsed} {:else} {/if} Event types ({selectedNotif.eventTypes.length})
{#if !isCollapsed} { formSelectedNotifications = formSelectedNotifications.map(n => n.id === channel.id ? { ...n, eventTypes: newTypes } : n ); }} /> {/if}
{/if}
{/each}
{/if} {:else}

Configure which notification channels should send alerts for events from this environment. {#if environment && !environment.collectActivity} Activity collection will be enabled automatically when you add a channel. {/if}

{#if envNotifLoading}
{:else} {#if envNotifications.length > 0}
{#each envNotifications as notif (notif.id)}
toggleChannelCollapse(notif.notificationId)} onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleChannelCollapse(notif.notificationId); } }} >
{#if collapsedChannels.has(notif.notificationId)} {:else} {/if} {#if notif.channelType === 'smtp'} {:else} {/if} {notif.channelName} ({notif.eventTypes.length} events)
e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}> environment && updateEnvNotification(environment.id, notif.notificationId, { enabled: !notif.enabled, eventTypes: notif.eventTypes })} />
{#if !notif.channelEnabled}

Channel disabled globally

{/if} {#if !collapsedChannels.has(notif.notificationId)}
{ environment && updateEnvNotification(environment.id, notif.notificationId, { enabled: notif.enabled, eventTypes: newTypes }); }} />
{/if}
{/each}
{:else}

No notification channels configured

Add a channel below to receive alerts for this environment

{/if} {@const availableChannels = notifications.filter(n => !envNotifications.some(en => en.notificationId === n.id))} {#if availableChannels.length > 0}
{#each availableChannels as channel} {/each}
{:else if notifications.length === 0}
{#if !$licenseStore.isEnterprise || $canAccess('notifications', 'create')} No notification channels have been created yet. Go to Settings → Notifications to add channels first. {:else} No notification channels have been created yet. Contact your administrator to configure notification channels. {/if}
{/if} {/if} {/if}
{#if !isEditing} {:else} {/if}