$cpu1"; if ($cpu2) $row2[] = ""; } if ($c) echo '
'; echo ""._('CPU').":".implode($row1); if ($row2) echo "
"._('HT').":".implode($row2); } } # ██████╗ ██████╗ ██████╗ ███████╗ # ██╔════╝██╔═══██╗██╔══██╗██╔════╝ # ██║ ██║ ██║██║ ██║█████╗ # ██║ ██║ ██║██║ ██║██╔══╝ # ╚██████╗╚██████╔╝██████╔╝███████╗ # ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ########################## ## CREATE CONTAINER ## ########################## if (isset($_POST['contName'])) { $postXML = postToXML($_POST, true); $dry_run = isset($_POST['dryRun']) && $_POST['dryRun']=='true'; $existing = _var($_POST,'existingContainer',false); $create_paths = $dry_run ? false : true; // Get the command line [$cmd, $Name, $Repository] = xmlToCommand($postXML, $create_paths); readfile("$docroot/plugins/dynamix.docker.manager/log.htm"); @flush(); // Saving the generated configuration file. $userTmplDir = $dockerManPaths['templates-user']; if (!is_dir($userTmplDir)) mkdir($userTmplDir, 0777, true); if ($Name) { $filename = sprintf('%s/my-%s.xml', $userTmplDir, $Name); if (is_file($filename)) { $oldXML = simplexml_load_file($filename); if ($oldXML->Icon != $_POST['contIcon']) { if (!strpos($Repository,":")) $Repository .= ":latest"; $iconPath = $DockerTemplates->getIcon($Repository,$Name); @unlink("$docroot/$iconPath"); @unlink("{$dockerManPaths['images']}/".basename($iconPath)); } } file_put_contents($filename, $postXML); } // Run dry if ($dry_run) { echo "

XML

"; echo "
".htmlspecialchars($postXML)."
"; echo "

COMMAND:

"; echo "
".htmlspecialchars($cmd)."
"; echo "
"; echo "

"; goto END; } // Will only pull image if it's absent if (!$DockerClient->doesImageExist($Repository)) { // Pull image if (!pullImage($Name, $Repository)) { echo '

'; goto END; } } $startContainer = true; // Remove existing container if ($DockerClient->doesContainerExist($Name)) { // attempt graceful stop of container first $oldContainerInfo = $DockerClient->getContainerDetails($Name); if (!empty($oldContainerInfo) && !empty($oldContainerInfo['State']) && !empty($oldContainerInfo['State']['Running'])) { // attempt graceful stop of container first stopContainer($Name); } // force kill container if still running after 10 seconds removeContainer($Name); } // Remove old container if renamed if ($existing && $DockerClient->doesContainerExist($existing)) { // determine if the container is still running $oldContainerInfo = $DockerClient->getContainerDetails($existing); if (!empty($oldContainerInfo) && !empty($oldContainerInfo['State']) && !empty($oldContainerInfo['State']['Running'])) { // attempt graceful stop of container first stopContainer($existing); } else { // old container was stopped already, ensure newly created container doesn't start up automatically $startContainer = false; } // force kill container if still running after 10 seconds removeContainer($existing,1); // remove old template if (strtolower($filename) != strtolower("$userTmplDir/my-$existing.xml")) { @unlink("$userTmplDir/my-$existing.xml"); } } // Extract real Entrypoint and Cmd from container for Tailscale if (isset($_POST['contTailscale']) && $_POST['contTailscale'] == 'on') { // Create preliminary base container but don't run it exec("/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/docker create --name '" . escapeshellarg($Name) . "' '" . escapeshellarg($Repository) . "'"); // Get Entrypoint and Cmd from docker inspect $containerInfo = $DockerClient->getContainerDetails($Name); $ts_env = isset($containerInfo['Config']['Entrypoint']) ? '-e ORG_ENTRYPOINT="' . implode(' ', $containerInfo['Config']['Entrypoint']) . '" ' : ''; $ts_env .= isset($containerInfo['Config']['Cmd']) ? '-e ORG_CMD="' . implode(' ', $containerInfo['Config']['Cmd']) . '" ' : ''; // Insert Entrypoint and Cmd to docker command $cmd = str_replace('-l net.unraid.docker.managed=dockerman', $ts_env . '-l net.unraid.docker.managed=dockerman' , $cmd); // Remove preliminary container exec("/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/docker rm '" . escapeshellarg($Name) . "'"); } if ($startContainer) $cmd = str_replace('/docker create ', '/docker run -d ', $cmd); execCommand($cmd); if ($startContainer) addRoute($Name); // add route for remote WireGuard access echo '

'; goto END; } ########################## ## UPDATE CONTAINER ## ########################## if (isset($_GET['updateContainer'])){ $echo = empty($_GET['mute']); if ($echo) { readfile("$docroot/plugins/dynamix.docker.manager/log.htm"); @flush(); } foreach ($_GET['ct'] as $value) { $tmpl = $DockerTemplates->getUserTemplate(unscript(urldecode($value))); if ($echo && !$tmpl) { echo ""; @flush(); continue; } $xml = file_get_contents($tmpl); [$cmd, $Name, $Repository] = xmlToCommand($tmpl); $Registry = getXmlVal($xml, "Registry"); $ExtraParams = getXmlVal($xml, "ExtraParams"); $Network = getXmlVal($xml, "Network"); $TS_Enabled = getXmlVal($xml, "TailscaleEnabled"); $oldImageID = $DockerClient->getImageID($Repository); // pull image if ($echo && !pullImage($Name, $Repository)) continue; $oldContainerInfo = $DockerClient->getContainerDetails($Name); // determine if the container is still running $startContainer = false; if (!empty($oldContainerInfo) && !empty($oldContainerInfo['State']) && !empty($oldContainerInfo['State']['Running'])) { // since container was already running, put it back it to a running state after update $cmd = str_replace('/docker create ', '/docker run -d ', $cmd); $startContainer = true; // attempt graceful stop of container first stopContainer($Name, false, $echo); } // check if network from another container is specified in xml (Network & ExtraParams) if (preg_match('/^container:(.*)/', $Network)) { $Net_Container = str_replace("container:", "", $Network); } else { preg_match("/--(net|network)=container:[^\s]+/", $ExtraParams, $NetworkParam); if (!empty($NetworkParam[0])) { $Net_Container = explode(':', $NetworkParam[0])[1]; $Net_Container = str_replace(['"', "'"], '', $Net_Container); } } // check if the container still exists from which the network should be used, if it doesn't exist any more recreate container with network none and don't start it if (!empty($Net_Container)) { $Net_Container_ID = $DockerClient->getContainerID($Net_Container); if (empty($Net_Container_ID)) { $cmd = str_replace('/docker run -d ', '/docker create ', $cmd); $cmd = preg_replace("/--(net|network)=(['\"]?)container:[^'\"]+\\2/", "--network=none ", $cmd); } } // force kill container if still running after time-out if (empty($_GET['communityApplications'])) removeContainer($Name, $echo); // Extract real Entrypoint and Cmd from container for Tailscale if ($TS_Enabled == 'true') { // Create preliminary base container but don't run it exec("/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/docker create --name '" . escapeshellarg($Name) . "' '" . escapeshellarg($Repository) . "'"); // Get Entrypoint and Cmd from docker inspect $containerInfo = $DockerClient->getContainerDetails($Name); $ts_env = isset($containerInfo['Config']['Entrypoint']) ? '-e ORG_ENTRYPOINT="' . implode(' ', $containerInfo['Config']['Entrypoint']) . '" ' : ''; $ts_env .= isset($containerInfo['Config']['Cmd']) ? '-e ORG_CMD="' . implode(' ', $containerInfo['Config']['Cmd']) . '" ' : ''; // Insert Entrypoint and Cmd to docker command $cmd = str_replace('-l net.unraid.docker.managed=dockerman', $ts_env . '-l net.unraid.docker.managed=dockerman' , $cmd); // Remove preliminary container exec("/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/docker rm '" . escapeshellarg($Name) . "'"); } execCommand($cmd, $echo); if ($startContainer) addRoute($Name); // add route for remote WireGuard access $DockerClient->flushCaches(); $newImageID = $DockerClient->getImageID($Repository); // remove old orphan image since it's no longer used by this container if ($oldImageID && $oldImageID != $newImageID) removeImage($oldImageID, $echo); } echo '

'; goto END; } ######################### ## REMOVE TEMPLATE ## ######################### if (isset($_POST['rmTemplate'])) { if (file_exists($_POST['rmTemplate']) && dirname($_POST['rmTemplate'])==$dockerManPaths['templates-user']) unlink($_POST['rmTemplate']); } ######################### ## LOAD TEMPLATE ## ######################### $xmlType = $xmlTemplate = ''; if (isset($_GET['xmlTemplate'])) { [$xmlType, $xmlTemplate] = my_explode(':', unscript(urldecode($_GET['xmlTemplate']))); if (is_file($xmlTemplate)) { $xml = xmlToVar($xmlTemplate); $templateName = $xml['Name']; if (preg_match('/^container:(.*)/', $xml['Network'])) { $xml['Network'] = explode(':', $xml['Network'], 2); } if ($xmlType == 'default') { if (!empty($dockercfg['DOCKER_APP_CONFIG_PATH']) && file_exists($dockercfg['DOCKER_APP_CONFIG_PATH'])) { // override /config foreach ($xml['Config'] as &$arrConfig) { if ($arrConfig['Type'] == 'Path' && strtolower($arrConfig['Target']) == '/config') { $arrConfig['Default'] = $arrConfig['Value'] = realpath($dockercfg['DOCKER_APP_CONFIG_PATH']).'/'.$xml['Name']; if (empty($arrConfig['Display']) || preg_match("/^Host Path\s\d/", $arrConfig['Name'])) { $arrConfig['Display'] = 'advanced-hide'; } if (empty($arrConfig['Name']) || preg_match("/^Host Path\s\d/", $arrConfig['Name'])) { $arrConfig['Name'] = 'AppData Config Path'; } } $arrConfig['Name'] = strip_tags(_var($arrConfig,'Name')); $arrConfig['Description'] = strip_tags(_var($arrConfig,'Description')); $arrConfig['Requires'] = strip_tags(_var($arrConfig,'Requires')); } } if (!empty($dockercfg['DOCKER_APP_UNRAID_PATH']) && file_exists($dockercfg['DOCKER_APP_UNRAID_PATH'])) { // override /unraid $boolFound = false; foreach ($xml['Config'] as &$arrConfig) { if ($arrConfig['Type'] == 'Path' && strtolower($arrConfig['Target']) == '/unraid') { $arrConfig['Default'] = $arrConfig['Value'] = realpath($dockercfg['DOCKER_APP_UNRAID_PATH']); $arrConfig['Display'] = 'hidden'; $arrConfig['Name'] = 'Unraid Share Path'; $boolFound = true; } } if (!$boolFound) { $xml['Config'][] = [ 'Name' => 'Unraid Share Path', 'Target' => '/unraid', 'Default' => realpath($dockercfg['DOCKER_APP_UNRAID_PATH']), 'Value' => realpath($dockercfg['DOCKER_APP_UNRAID_PATH']), 'Mode' => 'rw', 'Description' => '', 'Type' => 'Path', 'Display' => 'hidden', 'Required' => 'false', 'Mask' => 'false' ]; } } } $xml['Overview'] = str_replace(['[', ']'], ['<', '>'], $xml['Overview']); $xml['Description'] = $xml['Overview'] = strip_tags(str_replace("
","\n", $xml['Overview'])); echo ""; } } echo ""; $authoringMode = $dockercfg['DOCKER_AUTHORING_MODE'] == "yes" ? true : false; $authoring = $authoringMode ? 'advanced' : 'noshow'; $disableEdit = $authoringMode ? 'false' : 'true'; $showAdditionalInfo = ''; $bgcolor = $themeHelper->isLightTheme() ? '#f2f2f2' : '#1c1c1c'; // $themeHelper set in DefaultPageLayout.php # Search for existing TAILSCALE_ entries in the Docker template $TS_existing_vars = false; if (isset($xml["Config"]) && is_array($xml["Config"])) { foreach ($xml["Config"] as $config) { if (isset($config["Target"]) && strpos($config["Target"], "TAILSCALE_") === 0) { $TS_existing_vars = true; break; } } } # Try to detect port from WebUI and set webui_url $TSwebuiport = ''; $webui_url = ''; if (empty($xml['TailscalePort'])) { if (!empty($xml['WebUI'])) { $webui_url = parse_url($xml['WebUI']); preg_match('/:(\d+)\]/', $webui_url['host'], $matches); $TSwebuiport = $matches[1]; } } $TS_raw = []; $TS_container_raw = []; $TS_HostNameWarning = ""; $TS_HTTPSDisabledWarning = ""; $TS_ExitNodeNeedsApproval = false; $TS_MachinesLink = "https://login.tailscale.com/admin/machines/"; $TS_DirectMachineLink = $TS_MachinesLink; $TS_HostNameActual = ""; $TS_not_approved = ""; $TS_https_enabled = false; $ts_exit_nodes = []; $ts_en_check = false; // Get Tailscale information and create arrays/variables !empty($xml) && exec("docker exec -i " . escapeshellarg($xml['Name']) . " /bin/sh -c \"tailscale status --peers=false --json\"", $TS_raw); $TS_no_peers = json_decode(implode('', $TS_raw),true); $TS_container = json_decode(implode('', $TS_raw),true); $TS_container = $TS_container['Self']??''; # Look for Exit Nodes through Tailscale plugin (if installed) when container is not running if (empty($TS_container) && file_exists('/usr/local/sbin/tailscale') && exec('pgrep --ns $$ -f "/usr/local/sbin/tailscaled"')) { exec('tailscale exit-node list', $ts_exit_node_list, $retval); if ($retval === 0) { foreach ($ts_exit_node_list as $line) { if (!empty(trim($line))) { if (preg_match('/^(\d+\.\d+\.\d+\.\d+)\s+(.+)$/', trim($line), $matches)) { $parts = preg_split('/\s+/', $matches[2]); $ts_exit_nodes[] = [ 'ip' => $matches[1], 'hostname' => $parts[0], 'country' => $parts[1], 'city' => $parts[2], 'status' => $parts[3] ]; $ts_en_check = true; } } } } } if (!empty($TS_no_peers) && !empty($TS_container)) { // define the direct link to this machine on the Tailscale website if (!empty($TS_container['TailscaleIPs']) && !empty($TS_container['TailscaleIPs'][0])) { $TS_DirectMachineLink = $TS_MachinesLink.$TS_container['TailscaleIPs'][0]; } // warn if MagicDNS or HTTPS is disabled if (isset($TS_no_peers['Self']['Capabilities']) && is_array($TS_no_peers['Self']['Capabilities'])) { $TS_https_enabled = in_array("https", $TS_no_peers['Self']['Capabilities'], true) ? true : false; } if (empty($TS_no_peers['CurrentTailnet']['MagicDNSEnabled']) || !$TS_no_peers['CurrentTailnet']['MagicDNSEnabled'] || $TS_https_enabled !== true) { $TS_HTTPSDisabledWarning = "Enable HTTPS on your Tailscale account to use Tailscale Serve/Funnel."; } // In $TS_container, 'HostName' is what the user requested, need to parse 'DNSName' to find the actual HostName in use $TS_DNSName = _var($TS_container,'DNSName',''); $TS_HostNameActual = substr($TS_DNSName, 0, strpos($TS_DNSName, '.')); // compare the actual HostName in use to the one in the XML file if (strcasecmp($TS_HostNameActual, _var($xml, 'TailscaleHostname')) !== 0 && !empty($TS_DNSName)) { // they are different, show a warning $TS_HostNameWarning = "Warning: the actual Tailscale hostname is '".$TS_HostNameActual."'"; } // If this is an Exit Node, show warning if it still needs approval if (_var($xml,'TailscaleIsExitNode') == 'true' && _var($TS_container, 'ExitNodeOption') === false) { $TS_ExitNodeNeedsApproval = true; } //Check for key expiry if(!empty($TS_container['KeyExpiry'])) { $TS_expiry = new DateTime($TS_container['KeyExpiry']); $current_Date = new DateTime(); $TS_expiry_diff = $current_Date->diff($TS_expiry); } // Check for non approved routes if(!empty($xml['TailscaleRoutes'])) { $TS_advertise_routes = str_replace(' ', '', $xml['TailscaleRoutes']); if (empty($TS_container['PrimaryRoutes'])) { $TS_container['PrimaryRoutes'] = []; } $routes = explode(',', $TS_advertise_routes); foreach ($routes as $route) { if (!in_array($route, $TS_container['PrimaryRoutes'])) { $TS_not_approved .= " " . $route; } } } // Check for exit nodes if ts_en_check was not already done if (!$ts_en_check) { exec("docker exec -i ".$xml['Name']." /bin/sh -c \"tailscale exit-node list\"", $ts_exit_node_list, $retval); if ($retval === 0) { foreach ($ts_exit_node_list as $line) { if (!empty(trim($line))) { if (preg_match('/^(\d+\.\d+\.\d+\.\d+)\s+(.+)$/', trim($line), $matches)) { $parts = preg_split('/\s+/', $matches[2]); $ts_exit_nodes[] = [ 'ip' => $matches[1], 'hostname' => $parts[0], 'country' => $parts[1], 'city' => $parts[2], 'status' => $parts[3] ]; } } } } } // Construct WebUI URL on container template page // Check if webui_url, Tailscale WebUI and MagicDNS are not empty and make sure that MagicDNS is enabled if ( !empty($webui_url) && !empty($xml['TailscaleWebUI']) && (!empty($TS_no_peers['CurrentTailnet']['MagicDNSEnabled']) || ($TS_no_peers['CurrentTailnet']['MagicDNSEnabled']??false))) { // Check if serve or funnel are enabled by checking for [hostname] and replace string with TS_DNSName if (!empty($xml['TailscaleWebUI']) && strpos($xml['TailscaleWebUI'], '[hostname]') !== false && isset($TS_DNSName)) { $TS_webui_url = str_replace("[hostname][magicdns]", rtrim($TS_DNSName, '.'), $xml['TailscaleWebUI']); $TS_webui_url = preg_replace('/\[IP\]/', rtrim($TS_DNSName, '.'), $TS_webui_url); $TS_webui_url = preg_replace('/\[PORT:(\d{1,5})\]/', '443', $TS_webui_url); // Check if serve is disabled, construct url with port, path and query if present and replace [noserve] with url } elseif (strpos($xml['TailscaleWebUI'], '[noserve]') !== false && isset($TS_container['TailscaleIPs'])) { $ipv4 = ''; foreach ($TS_container['TailscaleIPs'] as $ip) { if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { $ipv4 = $ip; break; } } if (!empty($ipv4)) { $webui_url = isset($xml['WebUI']) ? parse_url($xml['WebUI']) : ''; $webui_port = (preg_match('/\[PORT:(\d+)\]/', $xml['WebUI'], $matches)) ? ':' . $matches[1] : ''; $webui_path = $webui_url['path'] ?? ''; $webui_query = isset($webui_url['query']) ? '?' . $webui_url['query'] : ''; $webui_query = preg_replace('/\[IP\]/', $ipv4, $webui_query); $webui_query = preg_replace('/\[PORT:(\d{1,5})\]/', ltrim($webui_port, ':'), $webui_query); $TS_webui_url = 'http://' . $ipv4 . $webui_port . $webui_path . $webui_query; } // Check if TailscaleWebUI in the xml is custom and display instead } elseif (strpos($xml['TailscaleWebUI'], '[hostname]') === false && strpos($xml['TailscaleWebUI'], '[noserve]') === false) { $TS_webui_url = $xml['TailscaleWebUI']; } } } ?> "> "> ">
doesContainerExist($templateName)):?>
_(Template)_: : :docker_client_general_help:
_(Name)_: : :docker_client_name_help:
_(Overview)_: :
_(Overview)_: : :docker_client_overview_help:
_(Additional Requirements)_: :
_(Additional Requirements)_: : :docker_client_additional_requirements_help:
_(Repository)_: : :docker_client_repository_help:
_(Categories)_: : _(Support Thread)_: : :docker_client_support_thread_help: _(Project Page)_: : :docker_client_project_page_help: _(Read Me First)_: : :docker_client_readme_help:
_(Registry URL)_: : :docker_client_hub_url_help:
Donation Text: : Donation Link: : Template URL: :
_(Icon URL)_: : :docker_client_icon_url_help: _(WebUI)_: : :docker_client_webui_help: _(Extra Parameters)_: : :docker_extra_parameters_help: _(Post Arguments)_: : :docker_post_arguments_help: _(CPU Pinning)_: : :docker_cpu_pinning_help:
_(Network Type)_: :
_(Fixed IP address)_ (_(optional)_): : :docker_fixed_ip_help:
_(Container Network)_: : :docker_container_network_help:

_(WARNING)_: : _(Existing TAILSCALE variables found, please remove any existing modifications in the Template for Tailscale before using this function!)_
_(First deployment)_: :

_(After deploying the container, open the log and follow the link to register the container to your Tailnet!)_

_(Recommendation)_: :

_(For the best experience with Tailscale, install "Tailscale (Plugin)" from)_ Community Applications.

_(Use Tailscale)_: : onchange="showTailscale(this)"> :docker_tailscale_help:
_(Use Tailscale)_: : _(Option disabled as Network type is not bridge or custom)_ :docker_tailscale_help:
_(NOTE)_: : _(This option will install Tailscale and dependencies into the container.)_
_(Warning)_: : Exit Node not yet approved. Navigate to the Tailscale website and approve it.
invert):?> _(Warning)_: : Tailscale Key expired! Renew/Disable key expiry for ''. _(Warning)_: : Tailscale Key will expire in days?> days! Disable Key Expiry for ''.
_(Warning)_: : The following route(s) are not approved:
_(Tailscale Hostname)_: : placeholder="_(Hostname for the container)_"> :docker_tailscale_hostname_help:
_(Be a Tailscale Exit Node)_: : :docker_tailscale_be_exitnode_help:
_(Use a Tailscale Exit Node)_: : placeholder="_(IP/Hostname from Exit Node)_" onchange="processExitNodeoptions(this)"> :docker_tailscale_exitnode_ip_help:
_(Tailscale Allow LAN Access)_: : :docker_tailscale_lanaccess_help:
_(Tailscale Userspace Networking)_: : :docker_tailscale_userspace_networking_help:
_(Enable Tailscale SSH)_: : :docker_tailscale_ssh_help:
_(Tailscale Serve)_: : ' . $TS_webui_url . ''; ?> :docker_tailscale_serve_mode_help:
_(Tailscale Serve Port)_: : :docker_tailscale_serve_port_help:
_(Tailscale Show Advanced Settings)_: : :docker_tailscale_show_advanced_help:
_(Tailscale Serve Target)_: : placeholder="_(Leave empty if unsure)_"> :docker_tailscale_serve_target_help:
_(Tailscale Serve Local Path)_: : placeholder="_(Leave empty if unsure)_"> :docker_tailscale_serve_local_path_help:
_(Tailscale Serve Protocol)_: : placeholder="_(Leave empty if unsure, defaults to https)_"> :docker_tailscale_serve_protocol_help:
_(Tailscale Serve Protocol Port)_: : placeholder="_(Leave empty if unsure, defaults to =443)_"> :docker_tailscale_serve_protocol_port_help:
_(Tailscale Serve Path)_: : placeholder="_(Leave empty if unsure)_"> :docker_tailscale_serve_path_help:
_(Tailscale WebUI)_: : > :docker_tailscale_serve_webui_help:
_(Tailscale Advertise Routes)_: : placeholder="_(Leave empty if unsure)_"> :docker_tailscale_advertise_routes_help:
_(Tailscale Accept Routes)_: : :docker_tailscale_accept_routes_help:
_(Tailscale Daemon Parameters)_: : placeholder="_(Leave empty if unsure)_"> :docker_tailscale_daemon_extra_params_help:
_(Tailscale Extra Parameters)_: : placeholder="_(Leave empty if unsure)_"> :docker_tailscale_extra_param_help:
_(Tailscale State Directory)_: : placeholder="_(Leave empty if unsure)_"> :docker_tailscale_statedir_help:
_(Tailscale Install Troubleshooting Packages)_: : > :docker_tailscale_troubleshooting_packages_help:

_(Console shell command)_: : _(Privileged)_: : :docker_privileged_help:
  : _(Show more settings)_ ...   : _(Show docker allocations)_ ...   : _(Add another Path, Port, Variable, Label or Device)_   : ">