diff --git a/Dockerfile b/Dockerfile index 51be6469..5fa0ee2c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,7 @@ WORKDIR /app # Set production ENV NODE_ENV=production -RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y curl unzip apache2-utils iproute2 && rm -rf /var/lib/apt/lists/* # Copy only the necessary files COPY --from=build /prod/dokploy/.next ./.next diff --git a/apps/dokploy/server/utils/docker.ts b/apps/dokploy/server/utils/docker.ts new file mode 100644 index 00000000..7577c844 --- /dev/null +++ b/apps/dokploy/server/utils/docker.ts @@ -0,0 +1,40 @@ +import { execAsync } from "@dokploy/server"; + +/** Returns if the current operating system is Windows Subsystem for Linux (WSL). */ +export const isWSL = async () => { + try { + const { stdout } = await execAsync("uname -r"); + const isWSL = stdout.includes("microsoft"); + return isWSL; + } catch (error) { + return false; + } +}; + +/** Returns the Docker host IP address. */ +export const getDockerHost = async (): Promise => { + if (process.env.NODE_ENV === "production") { + if (process.platform === "linux" && !isWSL()) { + try { + // Try to get the Docker bridge IP first + const { stdout } = await execAsync( + "ip route | awk '/default/ {print $3}'", + ); + + const hostIp = stdout.trim(); + if (!hostIp) { + throw new Error("Failed to get Docker host IP"); + } + + return hostIp; + } catch (error) { + console.error("Failed to get Docker host IP:", error); + return "172.17.0.1"; // Default Docker bridge network IP + } + } + + return "host.docker.internal"; + } + + return "localhost"; +}; diff --git a/apps/dokploy/server/wss/terminal.ts b/apps/dokploy/server/wss/terminal.ts index 8c661c8a..3f0d6a80 100644 --- a/apps/dokploy/server/wss/terminal.ts +++ b/apps/dokploy/server/wss/terminal.ts @@ -8,6 +8,23 @@ import { publicIpv4, publicIpv6 } from "public-ip"; import { Client, type ConnectConfig } from "ssh2"; import { WebSocketServer } from "ws"; import { setupLocalServerSSHKey } from "./utils"; +import { getDockerHost } from "../utils/docker"; + +const COMMAND_TO_ALLOW_LOCAL_ACCESS = ` +# ---------------------------------------- +mkdir -p $HOME/.ssh && \\ +chmod 700 $HOME/.ssh && \\ +touch $HOME/.ssh/authorized_keys && \\ +chmod 600 $HOME/.ssh/authorized_keys && \\ +cat /etc/dokploy/ssh/auto_generated-dokploy-local.pub >> $HOME/.ssh/authorized_keys && \\ +echo "āœ“ Dokploy SSH key added successfully. Reopen the terminal in Dokploy to reconnect." +# ----------------------------------------`; + +const COMMAND_TO_GRANT_PERMISSION_ACCESS = ` +# ---------------------------------------- +sudo chown -R $USER:$USER /etc/dokploy/ssh +# ---------------------------------------- +`; export const getPublicIpWithFallback = async () => { // @ts-ignore @@ -73,20 +90,41 @@ export const setupTerminalWebSocketServer = ( return; } - ws.send("Setting up private SSH key...\n"); - const privateKey = await setupLocalServerSSHKey(); + try { + ws.send("Setting up private SSH key...\n"); + const privateKey = await setupLocalServerSSHKey(); + + if (!privateKey) { + ws.close(); + return; + } + + const dockerHost = await getDockerHost(); + + ws.send(`Found Docker host: ${dockerHost}\n`); + + connectionDetails = { + host: dockerHost, + port, + username, + privateKey, + }; + } catch (error) { + console.error(`Error setting up private SSH key: ${error}`); + ws.send(`Error setting up private SSH key: ${error}\n`); + + if ( + error instanceof Error && + error.message.includes("Permission denied") + ) { + ws.send( + `Please run the following command on your server to grant permission access and then reopen this window to reconnect:${COMMAND_TO_GRANT_PERMISSION_ACCESS}`, + ); + } - if (!privateKey) { ws.close(); return; } - - connectionDetails = { - host: "localhost", - port, - username, - privateKey, - }; } else { const server = await findServerById(serverId); @@ -161,9 +199,15 @@ export const setupTerminalWebSocketServer = ( }) .on("error", (err) => { if (err.level === "client-authentication") { - ws.send( - `Authentication failed: Unauthorized ${isLocalServer ? "" : "private SSH key or "}username.\nāŒ Error: ${err.message} ${err.level}`, - ); + if (isLocalServer) { + ws.send( + `Authentication failed: Please run the command below on your server to allow access. Make sure to run it as the same user as the one configured in connection settings:${COMMAND_TO_ALLOW_LOCAL_ACCESS}\nAfter running the command, reopen this window to reconnect. This procedure is required only once.`, + ); + } else { + ws.send( + `Authentication failed: Unauthorized private SSH key or username.\nāŒ Error: ${err.message} ${err.level}`, + ); + } } else { ws.send(`SSH connection error: ${err.message} āŒ `); } diff --git a/apps/dokploy/server/wss/utils.ts b/apps/dokploy/server/wss/utils.ts index 9c78869d..cd50130c 100644 --- a/apps/dokploy/server/wss/utils.ts +++ b/apps/dokploy/server/wss/utils.ts @@ -1,18 +1,8 @@ -import { execAsync } from "@dokploy/server"; +import { execAsync, paths } from "@dokploy/server"; import os from "node:os"; import path from "node:path"; import fs from "node:fs"; -const HOME_PATH = process.env.HOME || process.env.USERPROFILE || "/"; - -const LOCAL_SSH_KEY_PATH = path.join( - HOME_PATH, - ".ssh", - "auto_generated-dokploy-local", -); - -const AUTHORIZED_KEYS_PATH = path.join(HOME_PATH, ".ssh", "authorized_keys"); - export const getShell = () => { switch (os.platform()) { case "win32": @@ -24,39 +14,18 @@ export const getShell = () => { } }; -/** Returns private SSH key for dokploy local server terminal. Uses already created SSH key or generates a new SSH key, also automatically appends the public key to `authorized_keys`, creating the file if needed. */ +/** Returns private SSH key for dokploy local server terminal. Uses already created SSH key or generates a new SSH key. +*/ export const setupLocalServerSSHKey = async () => { - try { - if (!fs.existsSync(LOCAL_SSH_KEY_PATH)) { - // Generate new SSH key if it hasn't been created yet - await execAsync( - `ssh-keygen -t rsa -b 4096 -f ${LOCAL_SSH_KEY_PATH} -N ""`, - ); - } + const { SSH_PATH } = paths(true); + const sshKeyPath = path.join(SSH_PATH, "auto_generated-dokploy-local"); - const privateKey = fs.readFileSync(LOCAL_SSH_KEY_PATH, "utf8"); - const publicKey = fs.readFileSync(`${LOCAL_SSH_KEY_PATH}.pub`, "utf8"); - const authKeyContent = `${publicKey}\n`; - - if (!fs.existsSync(AUTHORIZED_KEYS_PATH)) { - // Create authorized_keys if it doesn't exist yet - fs.writeFileSync(AUTHORIZED_KEYS_PATH, authKeyContent, { mode: 0o600 }); - return privateKey; - } - - const existingAuthKeys = fs.readFileSync(AUTHORIZED_KEYS_PATH, "utf8"); - if (existingAuthKeys.includes(publicKey)) { - return privateKey; - } - - // Append the public key to authorized_keys - fs.appendFileSync(AUTHORIZED_KEYS_PATH, authKeyContent, { - mode: 0o600, - }); - - return privateKey; - } catch (error) { - console.error("Error getting private SSH key for local terminal:", error); - return ""; + if (!fs.existsSync(sshKeyPath)) { + // Generate new SSH key if it hasn't been created yet + await execAsync(`ssh-keygen -t rsa -b 4096 -f ${sshKeyPath} -N "" -C "dokploy-local-access"`); } + + const privateKey = fs.readFileSync(sshKeyPath, "utf8"); + + return privateKey; };