From d699092753e1d7ff8aa7757d7016a970289931dc Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Mon, 5 Jan 2026 20:37:19 +0500 Subject: [PATCH 1/2] cli/docker commands update --- .../commands/wheels/docker/DockerCommand.cfc | 408 +++++--- cli/src/commands/wheels/docker/build.cfc | 78 +- cli/src/commands/wheels/docker/deploy.cfc | 344 +++---- cli/src/commands/wheels/docker/exec.cfc | 36 +- cli/src/commands/wheels/docker/init.cfc | 59 +- cli/src/commands/wheels/docker/login.cfc | 18 +- cli/src/commands/wheels/docker/logs.cfc | 44 +- cli/src/commands/wheels/docker/push.cfc | 201 ++-- cli/src/commands/wheels/docker/stop.cfc | 288 +----- .../commands/docker/docker-build.md | 643 ++----------- .../commands/docker/docker-deploy.md | 366 ++------ .../commands/docker/docker-exec.md | 883 ++---------------- .../commands/docker/docker-init.md | 44 +- .../commands/docker/docker-login.md | 667 +------------ .../commands/docker/docker-logs.md | 34 +- .../commands/docker/docker-push.md | 838 ++--------------- .../commands/docker/docker-stop.md | 771 +-------------- 17 files changed, 1193 insertions(+), 4529 deletions(-) diff --git a/cli/src/commands/wheels/docker/DockerCommand.cfc b/cli/src/commands/wheels/docker/DockerCommand.cfc index 0364c239b..657467034 100644 --- a/cli/src/commands/wheels/docker/DockerCommand.cfc +++ b/cli/src/commands/wheels/docker/DockerCommand.cfc @@ -19,26 +19,32 @@ component extends="../base" { string password="", boolean isLocal=true ) { - var local = {}; + local.command = ""; + local.username = arguments.username; + local.password = arguments.password; + local.image = arguments.image; + local.registryUrl = ""; switch(lCase(arguments.registry)) { case "dockerhub": - if (!len(trim(arguments.username))) { - error("Docker Hub username is required. Use --username="); + if (!len(trim(local.username))) { + print.line("Enter Docker Hub username:"); + local.username = ask(""); } print.yellowLine("Logging in to Docker Hub...").toConsole(); - if (!len(trim(arguments.password))) { + if (!len(trim(local.password))) { print.line("Enter Docker Hub password or access token:"); - arguments.password = ask(""); + local.password = ask(message="", mask="*"); } if (arguments.isLocal) { - local.loginCmd = ["docker", "login", "-u", arguments.username, "--password-stdin"]; - local.result = runLocalCommandWithInput(local.loginCmd, arguments.password); + local.execCmd = ["docker", "login", "-u", local.username, "--password-stdin"]; + local.result = runLocalCommandWithInput(local.execCmd, local.password); + local.command = ""; } else { - return "echo '" & arguments.password & "' | docker login -u " & arguments.username & " --password-stdin"; + local.command = "echo '" & local.password & "' | docker login -u " & local.username & " --password-stdin"; } break; @@ -48,104 +54,185 @@ component extends="../base" { // Extract region from image name if (!len(trim(arguments.image))) { - error("AWS ECR requires image path to determine region. Use --image=123456789.dkr.ecr.region.amazonaws.com/repo:tag"); + error("AWS ECR requires image name to determine region. Use --image=123456789.dkr.ecr.region.amazonaws.com/repo:tag"); } - local.region = extractAWSRegion(arguments.image); + + var region = extractAWSRegion(arguments.image); + print.cyanLine("Detected region: " & region).toConsole(); if (arguments.isLocal) { - // Get ECR login token - local.ecrCmd = ["aws", "ecr", "get-login-password", "--region", local.region]; - local.tokenResult = runLocalCommand(local.ecrCmd, false); + // aws ecr get-login-password --region region | docker login --username AWS --password-stdin account.dkr.ecr.region.amazonaws.com + // This is complex to run as a single array command due to pipes. + // Best to run AWS command to get password, then docker login? + // Or rely on shell=true if possible? CommandBox doesn't expose shell=true easily in generic calls. + // Let's print command for user to run manually if it fails? + // Or execute via shell wrapper? + // Windows: cmd /c "aws ... | docker login ..." + + var registryUrl = listFirst(arguments.image, "/"); - // Extract registry URL from image - local.registryUrl = listFirst(arguments.image, "/"); + if (server.os.name contains "Windows") { + local.loginCmd = ["cmd", "/c", "aws ecr get-login-password --region " & region & " | docker login --username AWS --password-stdin " & registryUrl]; + } else { + local.loginCmd = ["bash", "-c", "aws ecr get-login-password --region " & region & " | docker login --username AWS --password-stdin " & registryUrl]; + } - // Login to ECR - local.loginCmd = ["docker", "login", "--username", "AWS", "--password-stdin", local.registryUrl]; - local.result = runLocalCommandWithInput(local.loginCmd, local.tokenResult.output); + local.result = runLocalCommand(local.loginCmd); } else { - return "aws ecr get-login-password --region " & local.region & " | docker login --username AWS --password-stdin " & listFirst(arguments.image, "/"); + // Remote command + return "aws ecr get-login-password --region " & region & " | docker login --username AWS --password-stdin " & listFirst(arguments.image, "/"); } break; case "gcr": print.yellowLine("Logging in to Google Container Registry...").toConsole(); - print.cyanLine("Note: gcloud CLI must be configured").toConsole(); + local.keyFile = ""; + + if (fileExists(getCWD() & "/gcr-key.json")) { + local.keyFile = getCWD() & "/gcr-key.json"; + print.cyanLine("Found service account key: gcr-key.json").toConsole(); + } else { + print.line("Enter path to service account key file (JSON):"); + local.keyFile = ask(message=""); + } if (arguments.isLocal) { - runLocalCommand(["gcloud", "auth", "configure-docker"]); + if (server.os.name contains "Windows") { + local.loginCmd = ["cmd", "/c", "type " & local.keyFile & " | docker login -u _json_key --password-stdin https://gcr.io"]; + } else { + local.loginCmd = ["bash", "-c", "cat " & local.keyFile & " | docker login -u _json_key --password-stdin https://gcr.io"]; + } + local.result = runLocalCommand(local.loginCmd); } else { - return "gcloud auth configure-docker"; + // Start reading... + var keyContent = fileRead(local.keyFile); + // Compress JSON slightly to start + keyContent = replace(keyContent, chr(10), "", "all"); + keyContent = replace(keyContent, chr(13), "", "all"); + // Escape single quotes + keyContent = replace(keyContent, "'", "'\''", "all"); + + return "echo '" & keyContent & "' | docker login -u _json_key --password-stdin https://gcr.io"; } break; case "acr": - print.yellowLine("Logging in to Azure Container Registry...").toConsole(); - print.cyanLine("Note: Azure CLI must be configured").toConsole(); + // 1. Resolve URL first + local.registryUrl = ""; + if (len(trim(local.image)) && find("/", local.image)) { + local.registryUrl = listFirst(local.image, "/"); + } else { + var deployConfig = getDeployConfig(); + if (structKeyExists(deployConfig, "image") && len(trim(deployConfig.image)) && find("/", deployConfig.image)) { + local.image = deployConfig.image; + local.registryUrl = listFirst(local.image, "/"); + } else { + print.line("Enter Azure ACR Registry URL (e.g. myacr.azurecr.io):"); + local.registryUrl = ask(message=""); + if (!len(trim(local.registryUrl))) { + error("Azure ACR requires a registry URL."); + } + } + } + + // 2. Resolve Username + if (!len(trim(local.username))) { + print.line("Enter Azure ACR username:"); + local.username = ask(""); + } + + print.yellowLine("Logging in to Azure Container Registry: #local.registryUrl#").toConsole(); - // Extract registry name from image - if (!len(trim(arguments.image))) { - error("Azure ACR requires image path to determine registry. Use --image=registry.azurecr.io/image:tag"); + // 3. Resolve Password + if (!len(trim(local.password))) { + print.line("Enter ACR password:"); + local.password = ask(message="", mask="*"); } - local.registryName = listFirst(arguments.image, "."); if (arguments.isLocal) { - runLocalCommand(["az", "acr", "login", "--name", local.registryName]); + local.execCmd = ["docker", "login", local.registryUrl, "-u", local.username, "--password-stdin"]; + local.result = runLocalCommandWithInput(local.execCmd, local.password); + local.command = ""; } else { - return "az acr login --name " & local.registryName; + local.command = "echo '" & local.password & "' | docker login " & local.registryUrl & " -u " & local.username & " --password-stdin"; } break; case "ghcr": - if (!len(trim(arguments.username))) { - error("GitHub username is required. Use --username="); + if (!len(trim(local.username))) { + print.line("Enter GitHub username:"); + local.username = ask(""); } - print.yellowLine("Logging in to GitHub Container Registry...").toConsole(); - if (!len(trim(arguments.password))) { - print.line("Enter GitHub Personal Access Token:"); - arguments.password = ask(""); + if (!len(trim(local.password))) { + print.line("Enter Personal Access Token (PAT) with write:packages scope:"); + local.password = ask(message="", mask="*"); } if (arguments.isLocal) { - local.loginCmd = ["docker", "login", "ghcr.io", "-u", arguments.username, "--password-stdin"]; - local.result = runLocalCommandWithInput(local.loginCmd, arguments.password); + local.execCmd = ["docker", "login", "ghcr.io", "-u", local.username, "--password-stdin"]; + local.result = runLocalCommandWithInput(local.execCmd, local.password); + local.command = ""; } else { - return "echo '" & arguments.password & "' | docker login ghcr.io -u " & arguments.username & " --password-stdin"; + local.command = "echo '" & local.password & "' | docker login ghcr.io -u " & local.username & " --password-stdin"; } break; case "private": - if (!len(trim(arguments.username))) { - error("Registry username is required. Use --username="); + // 1. Resolve URL first + local.registryUrl = ""; + if (len(trim(local.image)) && find("/", local.image)) { + local.registryUrl = listFirst(local.image, "/"); + } else { + var deployConfig = getDeployConfig(); + if (structKeyExists(deployConfig, "image") && len(trim(deployConfig.image)) && find("/", deployConfig.image)) { + local.image = deployConfig.image; + local.registryUrl = listFirst(local.image, "/"); + } else { + print.line("Enter Private Registry URL (e.g. 192.168.1.10:5000 or registry.example.com):"); + local.registryUrl = ask(message=""); + if (!len(trim(local.registryUrl))) { + error("Private registry URL is required."); + } + } + } + + // 2. Resolve Username + if (!len(trim(local.username))) { + print.line("Enter registry username:"); + local.username = ask(""); } - print.yellowLine("Logging in to private registry...").toConsole(); + print.yellowLine("Logging in to private registry: #local.registryUrl#").toConsole(); - if (!len(trim(arguments.password))) { + // 3. Resolve Password + if (!len(trim(local.password))) { print.line("Enter registry password:"); - arguments.password = ask(""); - } - - local.registryUrl = ""; - if (len(trim(arguments.image))) { - local.registryUrl = listFirst(arguments.image, "/"); - } else { - error("Private registry requires image path to determine registry URL. Use --image=registry.example.com:port/image:tag"); + local.password = ask(message="", mask="*"); } if (arguments.isLocal) { - local.loginCmd = ["docker", "login", local.registryUrl, "-u", arguments.username, "--password-stdin"]; - local.result = runLocalCommandWithInput(local.loginCmd, arguments.password); + local.execCmd = ["docker", "login", local.registryUrl, "-u", local.username, "--password-stdin"]; + local.result = runLocalCommandWithInput(local.execCmd, local.password); + local.command = ""; } else { - return "echo '" & arguments.password & "' | docker login " & local.registryUrl & " -u " & arguments.username & " --password-stdin"; + local.command = "echo '" & local.password & "' | docker login " & local.registryUrl & " -u " & local.username & " --password-stdin"; } break; } - print.greenLine("Login successful").toConsole(); - return ""; + if (arguments.isLocal) { + print.greenLine("Login successful").toConsole(); + } + + return { + "command": local.command, + "username": local.username, + "password": local.password, + "image": local.image, + "registryUrl": local.registryUrl + }; } /** @@ -185,9 +272,41 @@ component extends="../base" { } /** - * Get project name from current directory + * Get project name from deploy.yml, box.json (slug/name), or current directory */ public function getProjectName() { + // 1. Try to read config/deploy.yml first (user's preferred source) + try { + var deployConfig = getDeployConfig(); + if (structKeyExists(deployConfig, "name") && len(trim(deployConfig.name))) { + return trim(deployConfig.name); + } + } catch (any e) {} + + // 2. Try to read box.json for unique identity + try { + var boxJsonPath = getCWD() & "/box.json"; + if (fileExists(boxJsonPath)) { + var boxJson = deserializeJSON(fileRead(boxJsonPath)); + + // Prioritize 'slug' + if (structKeyExists(boxJson, "slug") && len(trim(boxJson.slug))) { + return trim(boxJson.slug); + } + + // Fallback to 'name', slugified + if (structKeyExists(boxJson, "name") && len(trim(boxJson.name))) { + var pName = lCase(trim(boxJson.name)); + pName = reReplace(pName, "[^a-z0-9\-]", "-", "all"); + pName = reReplace(pName, "\-+", "-", "all"); + pName = reReplace(pName, "^\-|\-$", "", "all"); + if (len(pName)) return pName; + } + } + } catch (any e) { + // Ignore errors (missing file, invalid JSON), fall back to directory name + } + var cwd = getCWD(); var dirName = listLast(cwd, "\/"); dirName = lCase(dirName); @@ -197,6 +316,57 @@ component extends="../base" { return len(dirName) ? dirName : "wheels-app"; } + /** + * Read and parse config/deploy.yml + * Returns a struct with 'name', 'image', and 'servers' array + */ + public function getDeployConfig() { + var configPath = fileSystemUtil.resolvePath("config/deploy.yml"); + var config = { "name": "", "image": "", "servers": [] }; + + if (!fileExists(configPath)) { + return config; + } + + try { + var content = fileRead(configPath); + var lines = listToArray(content, chr(10)); + var currentServer = {}; + + for (var line in lines) { + var trimmedLine = trim(line); + if (!len(trimmedLine) || left(trimmedLine, 1) == "##") continue; + + // Simple YAML parser for the specific format we generate + if (find("name:", trimmedLine) == 1) { + config.name = trim(replace(trimmedLine, "name:", "")); + } else if (find("image:", trimmedLine) == 1) { + config.image = trim(replace(trimmedLine, "image:", "")); + } else if (find("- host:", trimmedLine)) { + // New server entry + if (!structIsEmpty(currentServer)) { + arrayAppend(config.servers, currentServer); + } + currentServer = { "host": trim(replace(trimmedLine, "- host:", "")), "port": 22 }; + } else if (find("user:", trimmedLine) && !structIsEmpty(currentServer)) { + currentServer.user = trim(replace(trimmedLine, "user:", "")); + } else if (find("port:", trimmedLine) && !structIsEmpty(currentServer)) { + currentServer.port = val(trim(replace(trimmedLine, "port:", ""))); + } else if (find("role:", trimmedLine) && !structIsEmpty(currentServer)) { + currentServer.role = trim(replace(trimmedLine, "role:", "")); + } + } + // Append last server + if (!structIsEmpty(currentServer)) { + arrayAppend(config.servers, currentServer); + } + } catch (any e) { + // Log error or ignore + } + + return config; + } + /** * Determine the final image name based on registry and parameters */ @@ -208,13 +378,25 @@ component extends="../base" { required string username, required string namespace ) { - // If custom image is specified, use it - if (len(trim(arguments.customImage))) { - return arguments.customImage; - } - // Use namespace if provided, otherwise use username local.prefix = len(trim(arguments.namespace)) ? arguments.namespace : arguments.username; + + // If custom image is specified + if (len(trim(arguments.customImage))) { + local.img = arguments.customImage; + + // If it's a naked image name (no slash) and we have a prefix, apply it (for Docker Hub/GHCR) + if (!find("/", local.img) && len(trim(local.prefix)) && !listFindNoCase("ecr,gcr,acr", arguments.registry)) { + local.img = local.prefix & "/" & local.img; + } + + // If it doesn't have a tag, append the provided tag + if (!find(":", local.img)) { + local.img &= ":" & arguments.tag; + } + + return local.img; + } // Build image name based on registry type switch(lCase(arguments.registry)) { @@ -231,7 +413,19 @@ component extends="../base" { error("GCR requires full image path. Use --image=gcr.io/project-id/image:tag"); case "acr": - error("Azure ACR requires full image path. Use --image=registry.azurecr.io/image:tag"); + local.registryUrl = ""; + // Try from deploy.yml + var deployConfig = getDeployConfig(); + if (structKeyExists(deployConfig, "image") && len(trim(deployConfig.image))) { + local.registryUrl = listFirst(deployConfig.image, "/"); + } else { + print.line("Enter Azure ACR Registry URL (e.g. myacr.azurecr.io):"); + local.registryUrl = ask(""); + if (!len(trim(local.registryUrl))) { + error("Azure ACR requires a registry URL to determine image path."); + } + } + return local.registryUrl & "/" & arguments.projectName & ":" & arguments.tag; case "ghcr": if (!len(trim(local.prefix))) { @@ -240,7 +434,25 @@ component extends="../base" { return "ghcr.io/" & lCase(local.prefix) & "/" & arguments.projectName & ":" & arguments.tag; case "private": - error("Private registry requires full image path. Use --image=registry.example.com:port/image:tag"); + local.registryUrl = ""; + // Try from deploy.yml + var deployConfig = getDeployConfig(); + if (structKeyExists(deployConfig, "image") && len(trim(deployConfig.image))) { + local.registryUrl = listFirst(deployConfig.image, "/"); + } else { + print.line("Enter Private Registry URL (e.g. 192.168.1.10:5000 or registry.example.com):"); + local.registryUrl = ask(""); + if (!len(trim(local.registryUrl))) { + error("Private registry requires a registry URL to determine image path."); + } + } + + local.finalImg = local.registryUrl & "/"; + if (len(trim(local.prefix))) { + local.finalImg &= local.prefix & "/"; + } + local.finalImg &= arguments.projectName & ":" & arguments.tag; + return local.finalImg; default: error("Unsupported registry type"); @@ -325,20 +537,25 @@ component extends="../base" { } local.exitCode = local.proc.waitFor(); + local.output = arrayToList(local.outputParts, chr(10)); if (local.exitCode neq 0) { - error("Login failed with exit code: " & local.exitCode); + error("Command failed with exit code: " & local.exitCode); } - return { exitCode: local.exitCode }; + return { exitCode: local.exitCode, output: local.output }; } - + /** - * Run a local system command interactively (inherits IO) - * Useful for long-running commands or those needing TTY (like logs -f) - * This prevents hanging by connecting subprocess IO directly to the console + * Run an interactive command (inheriting stdin/stdout) + * Note: This implementation in CommandBox 5+ can use run() with .toConsole() + * But for true interactivity (SSH), we need Java ProcessBuilder with inheritance */ public function runInteractiveCommand(array cmd, boolean inheritInput=false) { + // For simple output streaming without input interaction + // return runLocalCommand(arguments.cmd, true); + + // For true interactive shell (e.g. ssh, docker exec -it) var local = {}; local.javaCmd = createObject("java","java.util.ArrayList").init(); for (var c in arguments.cmd) { @@ -346,57 +563,14 @@ component extends="../base" { } local.pb = createObject("java","java.lang.ProcessBuilder").init(local.javaCmd); - - // Set working directory to current directory local.currentDir = createObject("java", "java.io.File").init(getCWD()); local.pb.directory(local.currentDir); - // Inherit Output and Error streams - var Redirect = createObject("java", "java.lang.ProcessBuilder$Redirect"); - local.pb.redirectOutput(Redirect.INHERIT); - local.pb.redirectError(Redirect.INHERIT); + // Inherit IO - this allows the command to take over the console + local.pb.inheritIO(); - // Conditionally inherit Input - // Only inherit input if explicitly requested (e.g. for interactive shells) - // Otherwise leave as PIPE to avoid CommandBox shell corruption - if (arguments.inheritInput) { - local.pb.redirectInput(Redirect.INHERIT); - } - - try { - local.proc = local.pb.start(); - local.exitCode = local.proc.waitFor(); - } catch (java.lang.InterruptedException e) { - // User interrupted (Ctrl+C) - local.exitCode = 130; // Standard exit code for SIGINT - - // Ensure process is destroyed - if (structKeyExists(local, "proc")) { - local.proc.destroy(); - } - - // Clear the interrupted status of the current thread to prevent side effects in CommandBox - createObject("java", "java.lang.Thread").currentThread().interrupt(); - // Actually, we want to clear it, so Thread.interrupted() does that. - // But waitFor() throws exception and clears status. - // Re-interrupting might cause CommandBox to think it's still interrupted. - // Let's just print a message. - print.line().toConsole(); - print.yellowLine("Command interrupted by user.").toConsole(); - } catch (any e) { - // Check for UserInterruptException (CommandBox specific) - if (findNoCase("UserInterruptException", e.message) || (structKeyExists(e, "type") && findNoCase("UserInterruptException", e.type))) { - local.exitCode = 130; - if (structKeyExists(local, "proc")) { - local.proc.destroy(); - } - print.line().toConsole(); - print.yellowLine("Command interrupted by user.").toConsole(); - } else { - local.exitCode = 1; - print.redLine("Error executing command: #e.message#").toConsole(); - } - } + local.proc = local.pb.start(); + local.exitCode = local.proc.waitFor(); return { exitCode: local.exitCode }; } diff --git a/cli/src/commands/wheels/docker/build.cfc b/cli/src/commands/wheels/docker/build.cfc index 96b30e044..70c2cbd4f 100644 --- a/cli/src/commands/wheels/docker/build.cfc +++ b/cli/src/commands/wheels/docker/build.cfc @@ -9,7 +9,7 @@ * wheels docker build --remote --servers=1,3 * {code} */ -component extends="../base" { +component extends="DockerCommand" { /** * @local Build Docker image on local machine @@ -102,7 +102,18 @@ component extends="../base" { // Get project name and determine tag local.projectName = getProjectName(); - local.imageTag = len(trim(arguments.customTag)) ? arguments.customTag : local.projectName & ":latest"; + local.deployConfig = getDeployConfig(); + local.baseImageName = (structKeyExists(local.deployConfig, "image") && len(trim(local.deployConfig.image))) ? local.deployConfig.image : local.projectName; + + if (len(trim(arguments.customTag))) { + if (find(":", arguments.customTag)) { + local.imageTag = arguments.customTag; + } else { + local.imageTag = local.baseImageName & ":" & arguments.customTag; + } + } else { + local.imageTag = local.baseImageName & ":latest"; + } print.cyanLine("Building image: " & local.imageTag).toConsole(); @@ -194,23 +205,42 @@ component extends="../base" { // Check for deploy-servers file (text or json) in current directory var textConfigPath = fileSystemUtil.resolvePath("deploy-servers.txt"); var jsonConfigPath = fileSystemUtil.resolvePath("deploy-servers.json"); + var ymlConfigPath = fileSystemUtil.resolvePath("config/deploy.yml"); var allServers = []; var serversToBuild = []; - - if (fileExists(textConfigPath)) { - print.cyanLine("Found deploy-servers.txt, loading server configuration").toConsole(); - allServers = loadServersFromTextFile("deploy-servers.txt"); - serversToBuild = filterServers(allServers, arguments.serverNumbers); - } else if (fileExists(jsonConfigPath)) { - print.cyanLine("Found deploy-servers.json, loading server configuration").toConsole(); - allServers = loadServersFromConfig("deploy-servers.json"); - serversToBuild = filterServers(allServers, arguments.serverNumbers); - } else { - error("No server configuration found. Create deploy-servers.txt or deploy-servers.json in your project root." & chr(10) & chr(10) & - "Example deploy-servers.txt:" & chr(10) & - "192.168.1.100 ubuntu 22" & chr(10) & - "production.example.com deploy" & chr(10) & chr(10) & - "Or see examples/deploy-servers.example.txt for more details."); + var projectName = getProjectName(); + + if (len(trim(arguments.serverNumbers)) == 0 && fileExists(ymlConfigPath)) { + var deployConfig = getDeployConfig(); + if (arrayLen(deployConfig.servers)) { + print.cyanLine("Found config/deploy.yml, loading server configuration").toConsole(); + allServers = deployConfig.servers; + + // Add default remoteDir if not present + for (var s in allServers) { + if (!structKeyExists(s, "remoteDir")) { + s.remoteDir = "/home/#s.user#/#projectName#"; + } + if (!structKeyExists(s, "port")) { + s.port = 22; + } + } + serversToBuild = allServers; + } + } + + if (arrayLen(serversToBuild) == 0) { + if (fileExists(textConfigPath)) { + print.cyanLine("Found deploy-servers.txt, loading server configuration").toConsole(); + allServers = loadServersFromTextFile("deploy-servers.txt"); + serversToBuild = filterServers(allServers, arguments.serverNumbers); + } else if (fileExists(jsonConfigPath)) { + print.cyanLine("Found deploy-servers.json, loading server configuration").toConsole(); + allServers = loadServersFromConfig("deploy-servers.json"); + serversToBuild = filterServers(allServers, arguments.serverNumbers); + } else { + error("No server configuration found. Use 'wheels docker init' or create deploy-servers.txt."); + } } if (arrayLen(serversToBuild) == 0) { @@ -377,7 +407,19 @@ component extends="../base" { print.yellowLine("Building Docker image...").toConsole(); // Determine tag - local.imageTag = len(trim(arguments.customTag)) ? arguments.customTag : local.imageName & ":latest"; + local.projectName = getProjectName(); + local.deployConfig = getDeployConfig(); + local.baseImageName = (structKeyExists(local.deployConfig, "image") && len(trim(local.deployConfig.image))) ? local.deployConfig.image : local.imageName; + + if (len(trim(arguments.customTag))) { + if (find(":", arguments.customTag)) { + local.imageTag = arguments.customTag; + } else { + local.imageTag = local.baseImageName & ":" & arguments.customTag; + } + } else { + local.imageTag = local.baseImageName & ":latest"; + } print.cyanLine("Building image: " & local.imageTag).toConsole(); local.buildCmd = "cd " & local.remoteDir & " && "; diff --git a/cli/src/commands/wheels/docker/deploy.cfc b/cli/src/commands/wheels/docker/deploy.cfc index e895e5d7a..2bc8fc3ca 100644 --- a/cli/src/commands/wheels/docker/deploy.cfc +++ b/cli/src/commands/wheels/docker/deploy.cfc @@ -21,6 +21,8 @@ component extends="DockerCommand" { * @servers Server configuration file (deploy-servers.txt or deploy-servers.json) - for remote deployment * @skipDockerCheck Skip Docker installation check on remote servers * @blueGreen Enable Blue/Green deployment strategy (zero downtime) - for remote deployment + * @image Deprecated. Use unique project name in box.json instead. + * @tag Custom tag to use (default: latest). Always treated as suffix to project name. */ function run( boolean local=false, @@ -31,13 +33,79 @@ component extends="DockerCommand" { boolean optimize=true, string servers="", boolean skipDockerCheck=false, - boolean blueGreen=false + boolean blueGreen=false, + string image="", + string tag="" ) { //ensure we are in a Wheels app requireWheelsApp(getCWD()); // Reconstruct arguments for handling --key=value style arguments = reconstructArgs(arguments); + var projectName = getProjectName(); + + // Interactive Tag Selection logic + // Only trigger if no tag is specified and we are running? + // Actually, if tag is empty, we usually default to 'latest'. + // But user requested: "check the images available with different tags and then ask the user to select" + + if (!len(arguments.tag)) { + try { + // List images for project with a safe delimiter + var imageCheck = runLocalCommand(["docker", "images", "--format", "{{.Repository}}:::{{.Tag}}"], false); + + if (imageCheck.exitCode == 0) { + var candidates = []; + var lines = listToArray(imageCheck.output, chr(10)); + + for (var img in lines) { + // Split by our custom delimiter + var parts = listToArray(img, ":::"); + if (arrayLen(parts) >= 2) { + var repo = trim(parts[1]); + var t = trim(parts[2]); + + // Check for exact match on project name + if (repo == projectName) { + arrayAppend(candidates, t); + } + } + } + + // Deduplicate candidates just in case + // (CFML doesn't have a native Set, so we can use a struct key trick or just leave it if docker output is unique enough) + + if (arrayLen(candidates) > 1) { + print.line().toConsole(); + print.boldCyanLine("Select a tag to deploy for project '#projectName#':").toConsole(); + + for (var i=1; i<=arrayLen(candidates); i++) { + print.line(" #i#. " & candidates[i]).toConsole(); + } + print.line().toConsole(); + + var selection = ask("Enter number to select, or press Enter for 'latest': "); + + if (len(trim(selection)) && isNumeric(selection) && selection > 0 && selection <= arrayLen(candidates)) { + arguments.tag = candidates[selection]; + print.greenLine("Selected tag: " & arguments.tag).toConsole(); + } else if (len(trim(selection))) { + // Treat as custom tag input if they typed a string not in the list? + // Or just fallback to what they typed + arguments.tag = selection; + print.greenLine("Using custom tag: " & arguments.tag).toConsole(); + } else { + // Empty selection matches 'latest' default logic later, or we can explicit set it + print.yellowLine("No selection made, defaulting to 'latest'").toConsole(); + } + } + } + } catch (any e) { + // Determine if we should show error or just fail silently to defaults + // print.redLine("Warning: Failed to list local images: " & e.message).toConsole(); + } + } + // set local as default if neither specified if (!arguments.local && !arguments.remote) { arguments.local=true; @@ -49,9 +117,9 @@ component extends="DockerCommand" { // Route to appropriate deployment method if (arguments.local) { - deployLocal(arguments.environment, arguments.db, arguments.cfengine, arguments.optimize); + deployLocal(arguments.environment, arguments.db, arguments.cfengine, arguments.optimize, arguments.tag); } else { - deployRemote(arguments.servers, arguments.skipDockerCheck, arguments.blueGreen); + deployRemote(arguments.servers, arguments.skipDockerCheck, arguments.blueGreen, arguments.tag); } } @@ -63,7 +131,8 @@ component extends="DockerCommand" { string environment, string db, string cfengine, - boolean optimize + boolean optimize, + string tag="" ) { // Welcome message print.line(); @@ -76,29 +145,18 @@ component extends="DockerCommand" { if (local.useCompose) { print.greenLine("Found docker-compose file, will use docker-compose").toConsole(); - // Check if Docker is installed locally - if (!isDockerInstalled()) { - error("Docker is not installed or not accessible. Please ensure Docker Desktop or Docker Engine is running."); - } - - print.yellowLine("Starting services with docker-compose...").toConsole(); - - try { - // Stop existing containers - runLocalCommand(["docker", "compose", "down"]); - } catch (any e) { - print.yellowLine("No existing containers to stop").toConsole(); + // Just run docker-compose up + if (len(arguments.tag)) { + print.yellowLine("Note: --tag argument is ignored when using docker-compose.").toConsole(); } - // Start containers with build - print.yellowLine("Building and starting containers...").toConsole(); - runLocalCommand(["docker", "compose", "up", "-d", "--build"]); + print.yellowLine("Starting services...").toConsole(); + runLocalCommand(["docker-compose", "up", "-d", "--build"]); + print.line(); + print.boldGreenLine("Services started successfully!").toConsole(); print.line(); - print.boldGreenLine("Docker Compose services started successfully!").toConsole(); - print.line(); - print.yellowLine("Check container status with: docker compose ps").toConsole(); - print.yellowLine("View logs with: docker compose logs -f").toConsole(); + print.yellowLine("View logs with: docker-compose logs -f").toConsole(); print.line(); } else { @@ -125,32 +183,49 @@ component extends="DockerCommand" { } // Get project name for image/container naming - local.imageName = getProjectName(); + local.projectName = getProjectName(); + local.deployConfig = getDeployConfig(); + + // Strict Tag Strategy: projectName:tag + local.tag = len(arguments.tag) ? arguments.tag : "latest"; - print.yellowLine("Building Docker image...").toConsole(); + // Smart Tag Logic: Check if tag contains colon (full image name) + if (find(":", local.tag)) { + local.imageName = local.tag; + } else if (structKeyExists(local.deployConfig, "image") && len(trim(local.deployConfig.image))) { + local.imageName = local.deployConfig.image & ":" & local.tag; + } else { + local.imageName = local.projectName & ":" & local.tag; + } + + // Container Name: Always use project name for consistency + local.containerName = local.projectName; + + print.yellowLine("Building Docker image (" & local.imageName & ")...").toConsole(); runLocalCommand(["docker", "build", "-t", local.imageName, "."]); print.yellowLine("Starting container...").toConsole(); try { // Stop and remove existing container - runLocalCommand(["docker", "stop", local.imageName]); - runLocalCommand(["docker", "rm", local.imageName]); + runLocalCommand(["docker", "stop", local.containerName]); + runLocalCommand(["docker", "rm", local.containerName]); } catch (any e) { print.yellowLine("No existing container to remove").toConsole(); } // Run new container - runLocalCommand(["docker", "run", "-d", "--name", local.imageName, "-p", local.exposedPort & ":" & local.exposedPort, local.imageName]); + runLocalCommand(["docker", "run", "-d", "--name", local.containerName, "-p", local.exposedPort & ":" & local.exposedPort, local.imageName]); print.line(); print.boldGreenLine("Container started successfully!").toConsole(); print.line(); - print.yellowLine("Container name: " & local.imageName).toConsole(); + print.yellowLine("Image: " & local.imageName).toConsole(); + print.yellowLine("Container: " & local.containerName).toConsole(); print.yellowLine("Access your application at: http://localhost:" & local.exposedPort).toConsole(); print.line(); print.yellowLine("Check container status with: docker ps").toConsole(); - print.yellowLine("View logs with: docker logs -f " & local.imageName).toConsole(); + print.yellowLine("View logs with: wheels docker logs --local").toConsole(); print.line(); } } @@ -171,11 +246,13 @@ component extends="DockerCommand" { // REMOTE DEPLOYMENT // ============================================================================= - private function deployRemote(string serversFile, boolean skipDockerCheck, boolean blueGreen) { + private function deployRemote(string serversFile, boolean skipDockerCheck, boolean blueGreen, string tag="") { // Check for deploy-servers file (text or json) in current directory var textConfigPath = fileSystemUtil.resolvePath("deploy-servers.txt"); var jsonConfigPath = fileSystemUtil.resolvePath("deploy-servers.json"); + var ymlConfigPath = fileSystemUtil.resolvePath("config/deploy.yml"); var servers = []; + var projectName = getProjectName(); // If specific servers file is provided, use that if (len(trim(arguments.serversFile))) { @@ -190,7 +267,25 @@ component extends="DockerCommand" { servers = loadServersFromTextFile(arguments.serversFile); } } - // Otherwise, look for default files + // 1. Look for config/deploy.yml first + else if (fileExists(ymlConfigPath)) { + var deployConfig = getDeployConfig(); + if (arrayLen(deployConfig.servers)) { + print.cyanLine("Found config/deploy.yml, loading server configuration").toConsole(); + servers = deployConfig.servers; + + // Add defaults for missing fields + for (var s in servers) { + if (!structKeyExists(s, "remoteDir")) { + s.remoteDir = "/home/#s.user#/#projectName#"; + } + if (!structKeyExists(s, "port")) { + s.port = 22; + } + } + } + } + // 2. Otherwise, look for default files else if (fileExists(textConfigPath)) { print.cyanLine("Found deploy-servers.txt, loading server configuration").toConsole(); servers = loadServersFromTextFile("deploy-servers.txt"); @@ -198,11 +293,7 @@ component extends="DockerCommand" { print.cyanLine("Found deploy-servers.json, loading server configuration").toConsole(); servers = loadServersFromConfig("deploy-servers.json"); } else { - error("No server configuration found. Create deploy-servers.txt or deploy-servers.json in your project root." & chr(10) & chr(10) & - "Example deploy-servers.txt:" & chr(10) & - "192.168.1.100 ubuntu 22" & chr(10) & - "production.example.com deploy" & chr(10) & chr(10) & - "Or see examples/deploy-servers.example.txt for more details."); + error("No server configuration found. Use 'wheels docker init' or create deploy-servers.txt."); } if (arrayLen(servers) == 0) { @@ -215,7 +306,7 @@ component extends="DockerCommand" { } // Deploy to all servers sequentially - deployToMultipleServersSequential(servers, arguments.skipDockerCheck, arguments.blueGreen); + deployToMultipleServersSequential(servers, arguments.skipDockerCheck, arguments.blueGreen, arguments.tag); print.line().boldGreenLine("Deployment to all servers completed!").toConsole(); } @@ -223,13 +314,22 @@ component extends="DockerCommand" { /** * Deploy to multiple servers sequentially */ - private function deployToMultipleServersSequential(required array servers, boolean skipDockerCheck, boolean blueGreen) { + private function deployToMultipleServersSequential(required array servers, boolean skipDockerCheck, boolean blueGreen, string tag="") { var successCount = 0; var failureCount = 0; var serverConfig = {}; for (var i = 1; i <= arrayLen(servers); i++) { serverConfig = servers[i]; + + // Override tag if provided via CLI argument + if (len(arguments.tag)) { + serverConfig.tag = arguments.tag; + } else if (!structKeyExists(serverConfig, "tag")) { + // Default tag is latest if not specified in server config either + serverConfig.tag = "latest"; + } + print.line().boldCyanLine("---------------------------------------").toConsole(); print.boldCyanLine("Deploying to server #i# of #arrayLen(servers)#: #serverConfig.host#").toConsole(); print.line().boldCyanLine("---------------------------------------").toConsole(); @@ -247,15 +347,8 @@ component extends="DockerCommand" { print.redLine("Failed to deploy to #serverConfig.host#: #e.message#").toConsole(); } } - - print.line().toConsole(); - print.boldCyanLine("Deployment Summary:").toConsole(); - print.greenLine(" Successful: #successCount#").toConsole(); - if (failureCount > 0) { - print.redLine(" Failed: #failureCount#").toConsole(); - } } - + /** * Deploy to a single server (Standard Strategy) */ @@ -264,8 +357,23 @@ component extends="DockerCommand" { local.host = arguments.serverConfig.host; local.user = arguments.serverConfig.user; local.port = structKeyExists(arguments.serverConfig, "port") ? arguments.serverConfig.port : 22; - local.remoteDir = structKeyExists(arguments.serverConfig, "remoteDir") ? arguments.serverConfig.remoteDir : "/home/#local.user#/#local.user#-app"; - local.imageName = structKeyExists(arguments.serverConfig, "imageName") ? arguments.serverConfig.imageName : "#local.user#-app"; + local.projectName = getProjectName(); // Use unique project name + + // Use standard directory based on Project Name + local.remoteDir = structKeyExists(arguments.serverConfig, "remoteDir") ? arguments.serverConfig.remoteDir : "/home/#local.user#/#local.projectName#"; + + local.tag = structKeyExists(arguments.serverConfig, "tag") ? arguments.serverConfig.tag : "latest"; + local.deployConfig = getDeployConfig(); + + // Smart Tag Logic + if (find(":", local.tag)) { + local.imageName = local.tag; + } else if (structKeyExists(local.deployConfig, "image") && len(trim(local.deployConfig.image))) { + local.imageName = local.deployConfig.image & ":" & local.tag; + } else { + local.imageName = local.projectName & ":" & local.tag; + } + local.containerName = local.projectName; // Step 1: Check SSH connection if (!testSSHConnection(local.host, local.user, local.port)) { @@ -324,45 +432,37 @@ component extends="DockerCommand" { local.deployScript &= "cd " & local.remoteDir & chr(10); if (local.useCompose) { - // Use docker-compose with proper permissions - local.deployScript &= "echo 'Starting services with docker-compose...'" & chr(10); - - // Check if user is in docker group and can run docker without sudo + // Use docker-compose local.deployScript &= "if groups | grep -q docker && [ -w /var/run/docker.sock ]; then" & chr(10); - local.deployScript &= " ## User has docker access, run without sudo" & chr(10); local.deployScript &= " docker compose down || true" & chr(10); local.deployScript &= " docker compose up -d --build" & chr(10); local.deployScript &= "else" & chr(10); - local.deployScript &= " ## User needs sudo for docker" & chr(10); local.deployScript &= " sudo docker compose down || true" & chr(10); local.deployScript &= " sudo docker compose up -d --build" & chr(10); local.deployScript &= "fi" & chr(10); - local.deployScript &= "echo 'Docker Compose services started!'" & chr(10); } else { - // Use standard docker commands with proper permissions + // Use standard docker commands local.deployScript &= "echo 'Building Docker image...'" & chr(10); - // Check if user is in docker group and can run docker without sudo + // Check if user is in docker group local.deployScript &= "if groups | grep -q docker && [ -w /var/run/docker.sock ]; then" & chr(10); - local.deployScript &= " ## User has docker access, run without sudo" & chr(10); local.deployScript &= " docker build -t " & local.imageName & " ." & chr(10); local.deployScript &= " echo 'Starting container...'" & chr(10); - local.deployScript &= " docker stop " & local.imageName & " || true" & chr(10); - local.deployScript &= " docker rm " & local.imageName & " || true" & chr(10); - local.deployScript &= " docker run -d --name " & local.imageName & " -p " & local.exposedPort & ":" & local.exposedPort & " " & local.imageName & chr(10); + local.deployScript &= " docker stop " & local.containerName & " || true" & chr(10); + local.deployScript &= " docker rm " & local.containerName & " || true" & chr(10); + local.deployScript &= " docker run -d --name " & local.containerName & " -p " & local.exposedPort & ":" & local.exposedPort & " " & local.imageName & chr(10); local.deployScript &= "else" & chr(10); - local.deployScript &= " ## User needs sudo for docker" & chr(10); local.deployScript &= " sudo docker build -t " & local.imageName & " ." & chr(10); local.deployScript &= " echo 'Starting container...'" & chr(10); - local.deployScript &= " sudo docker stop " & local.imageName & " || true" & chr(10); - local.deployScript &= " sudo docker rm " & local.imageName & " || true" & chr(10); - local.deployScript &= " sudo docker run -d --name " & local.imageName & " -p " & local.exposedPort & ":" & local.exposedPort & " " & local.imageName & chr(10); + local.deployScript &= " sudo docker stop " & local.containerName & " || true" & chr(10); + local.deployScript &= " sudo docker rm " & local.containerName & " || true" & chr(10); + local.deployScript &= " sudo docker run -d --name " & local.containerName & " -p " & local.exposedPort & ":" & local.exposedPort & " " & local.imageName & chr(10); local.deployScript &= "fi" & chr(10); } local.deployScript &= "echo 'Deployment complete!'" & chr(10); - // Normalize line endings + // Normalize local.deployScript = replace(local.deployScript, chr(13) & chr(10), chr(10), "all"); local.deployScript = replace(local.deployScript, chr(13), chr(10), "all"); @@ -377,7 +477,7 @@ component extends="DockerCommand" { fileDelete(local.tempFile); print.yellowLine("Executing deployment script remotely...").toConsole(); - // Use interactive command to prevent hanging and allow Ctrl+C + // Use interactive command var execCmd = ["ssh", "-p", local.port]; execCmd.addAll(getSSHOptions()); execCmd.addAll([local.user & "@" & local.host, "chmod +x /tmp/deploy-simple.sh && bash /tmp/deploy-simple.sh"]); @@ -395,8 +495,11 @@ component extends="DockerCommand" { local.host = arguments.serverConfig.host; local.user = arguments.serverConfig.user; local.port = structKeyExists(arguments.serverConfig, "port") ? arguments.serverConfig.port : 22; - local.remoteDir = structKeyExists(arguments.serverConfig, "remoteDir") ? arguments.serverConfig.remoteDir : "/home/#local.user#/#local.user#-app"; - local.imageName = structKeyExists(arguments.serverConfig, "imageName") ? arguments.serverConfig.imageName : "#local.user#-app"; + local.projectName = getProjectName(); + local.remoteDir = structKeyExists(arguments.serverConfig, "remoteDir") ? arguments.serverConfig.remoteDir : "/home/#local.user#/#local.projectName#"; + + local.tag = structKeyExists(arguments.serverConfig, "tag") ? arguments.serverConfig.tag : "latest"; + local.imageName = local.projectName; // Just project name, tag is separate variable in B/G script // Step 1: Check SSH connection if (!testSSHConnection(local.host, local.user, local.port)) { @@ -404,7 +507,7 @@ component extends="DockerCommand" { } print.greenLine("SSH connection successful").toConsole(); - // Step 1.5: Check and install Docker if needed + // Step 1.5: Check and install Docker if (!arguments.skipDockerCheck) { ensureDockerInstalled(local.host, local.user, local.port); } @@ -449,6 +552,7 @@ component extends="DockerCommand" { local.deployScript &= "REMOTE_TAR='" & local.remoteTar & "'" & chr(10); local.deployScript &= "NETWORK_NAME='web'" & chr(10); local.deployScript &= "PROXY_NAME='nginx-proxy'" & chr(10); + local.deployScript &= "TAG='" & local.tag & "'" & chr(10); // Extract source local.deployScript &= "echo 'Extracting source to ' $REMOTE_DIR ' ...'" & chr(10); @@ -458,7 +562,7 @@ component extends="DockerCommand" { // Build Image local.deployScript &= "echo 'Building Docker image...'" & chr(10); - local.deployScript &= "docker build -t $APP_NAME:latest ." & chr(10); + local.deployScript &= "docker build -t $APP_NAME:$TAG ." & chr(10); // Ensure Network Exists local.deployScript &= "echo 'Ensuring Docker network exists...'" & chr(10); @@ -491,15 +595,15 @@ component extends="DockerCommand" { local.deployScript &= "echo 'Current active color: ' $CURRENT_COLOR" & chr(10); local.deployScript &= "echo 'Deploying to: ' $TARGET_COLOR" & chr(10); - // Stop Target if exists (cleanup from failed deploy or old state) + // Stop Target if exists local.deployScript &= "docker stop $TARGET_CONTAINER 2>/dev/null || true" & chr(10); local.deployScript &= "docker rm $TARGET_CONTAINER 2>/dev/null || true" & chr(10); // Start New Container local.deployScript &= "echo 'Starting ' $TARGET_CONTAINER ' ...'" & chr(10); - local.deployScript &= "docker run -d --name $TARGET_CONTAINER --network $NETWORK_NAME --restart unless-stopped $APP_NAME:latest" & chr(10); + local.deployScript &= "docker run -d --name $TARGET_CONTAINER --network $NETWORK_NAME --restart unless-stopped $APP_NAME:$TAG" & chr(10); - // Wait for container to be ready (simple sleep for now, could be curl loop) + // Wait for container local.deployScript &= "echo 'Waiting for container to initialize...'" & chr(10); local.deployScript &= "sleep 5" & chr(10); @@ -516,7 +620,6 @@ component extends="DockerCommand" { local.deployScript &= "}" & chr(10); local.deployScript &= "EOF" & chr(10); - // Copy config to nginx container and reload local.deployScript = replace(local.deployScript, chr(13), chr(10), "all"); local.tempFile = getTempFile(getTempDirectory(), "deploy_bg_"); @@ -530,7 +633,6 @@ component extends="DockerCommand" { fileDelete(local.tempFile); print.yellowLine("Executing Blue/Green deployment script remotely...").toConsole(); - // Use interactive command to prevent hanging and allow Ctrl+C var execCmd = ["ssh", "-p", local.port]; execCmd.addAll(getSSHOptions()); execCmd.addAll([local.user & "@" & local.host, "chmod +x /tmp/deploy-bluegreen.sh && bash /tmp/deploy-bluegreen.sh"]); @@ -539,11 +641,7 @@ component extends="DockerCommand" { print.boldGreenLine("Blue/Green Deployment to #local.host# completed successfully!").toConsole(); } - - // ============================================================================= - // HELPER FUNCTIONS - // ============================================================================= - + /** * Check if Docker is installed on remote server and install if needed */ @@ -592,40 +690,6 @@ component extends="DockerCommand" { if (local.sudoCheckResult.exitCode neq 0) { print.line().toConsole(); print.boldRedLine("ERROR: User '#arguments.user#' does not have passwordless sudo access on #arguments.host#!").toConsole(); - print.line().toConsole(); - print.yellowLine("To enable passwordless sudo for Docker installation, follow these steps:").toConsole(); - print.line().toConsole(); - print.cyanLine(" 1. SSH into the server:").toConsole(); - print.boldWhiteLine(" ssh " & arguments.user & "@" & arguments.host & (arguments.port neq 22 ? " -p " & arguments.port : "")).toConsole(); - print.line().toConsole(); - print.cyanLine(" 2. Edit the sudoers file:").toConsole(); - print.boldWhiteLine(" sudo visudo").toConsole(); - print.line().toConsole(); - print.cyanLine(" 3. Add this line at the end of the file:").toConsole(); - print.boldWhiteLine(" " & arguments.user & " ALL=(ALL) NOPASSWD:ALL").toConsole(); - print.line().toConsole(); - print.cyanLine(" 4. Save and exit:").toConsole(); - print.line(" - Press Ctrl+X").toConsole(); - print.line(" - Press Y to confirm").toConsole(); - print.line(" - Press Enter to save").toConsole(); - print.line().toConsole(); - print.yellowLine("OR, manually install Docker on the remote server:").toConsole(); - print.line().toConsole(); - print.cyanLine(" For Ubuntu/Debian:").toConsole(); - print.line(" curl -fsSL https://get.docker.com -o get-docker.sh").toConsole(); - print.line(" sudo sh get-docker.sh").toConsole(); - print.line(" sudo usermod -aG docker " & arguments.user).toConsole(); - print.line(" newgrp docker").toConsole(); - print.line().toConsole(); - print.cyanLine(" For CentOS/RHEL:").toConsole(); - print.line(" curl -fsSL https://get.docker.com -o get-docker.sh").toConsole(); - print.line(" sudo sh get-docker.sh").toConsole(); - print.line(" sudo usermod -aG docker " & arguments.user).toConsole(); - print.line(" newgrp docker").toConsole(); - print.line().toConsole(); - print.boldYellowLine("After configuring passwordless sudo or installing Docker, run the deployment again.").toConsole(); - print.line().toConsole(); - error("Cannot install Docker: User '" & arguments.user & "' requires passwordless sudo access on " & arguments.host); } @@ -673,10 +737,9 @@ component extends="DockerCommand" { fileDelete(local.tempFile); // Execute install script - print.yellowLine("Installing Docker (this may take a few minutes)...").toConsole(); + print.yellowLine("Installing Docker...").toConsole(); var installCmd = ["ssh", "-p", arguments.port]; installCmd.addAll(getSSHOptions()); - // Increase timeout for installation installCmd.addAll(["-o", "ServerAliveInterval=30", "-o", "ServerAliveCountMax=10"]); installCmd.addAll([arguments.user & "@" & arguments.host, "sudo bash /tmp/install-docker.sh"]); @@ -744,46 +807,22 @@ component extends="DockerCommand" { private function getDockerInstallScriptDebian() { var script = '##!/bin/bash set -e - -echo "Installing Docker on Debian/Ubuntu..." - -## Set non-interactive mode export DEBIAN_FRONTEND=noninteractive - -## Update package index apt-get update - -## Install prerequisites apt-get install -y ca-certificates curl gnupg lsb-release - -## Add Docker GPG key mkdir -p /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg - -## Set up repository echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null - -## Install Docker with automatic yes to all prompts apt-get update apt-get install -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin - -## Start and enable Docker systemctl start docker systemctl enable docker - -## Wait for Docker to be ready sleep 3 - -## Add current user to docker group (determine actual user if running via sudo) ACTUAL_USER="${SUDO_USER:-$USER}" if [ -n "$ACTUAL_USER" ] && [ "$ACTUAL_USER" != "root" ]; then usermod -aG docker $ACTUAL_USER - echo "Added user $ACTUAL_USER to docker group" fi - -## Set proper permissions on docker socket chmod 666 /var/run/docker.sock - echo "Docker installation completed successfully!" '; return script; @@ -795,38 +834,19 @@ echo "Docker installation completed successfully!" private function getDockerInstallScriptRHEL() { var script = '##!/bin/bash set -e - -echo "Installing Docker on RHEL/CentOS/Fedora..." - -## Install prerequisites yum install -y yum-utils - -## Add Docker repository yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo - -## Install Docker yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin - -## Start and enable Docker systemctl start docker systemctl enable docker - -## Wait for Docker to be ready sleep 3 - -## Add current user to docker group ACTUAL_USER="${SUDO_USER:-$USER}" if [ -n "$ACTUAL_USER" ] && [ "$ACTUAL_USER" != "root" ]; then usermod -aG docker $ACTUAL_USER - echo "Added user $ACTUAL_USER to docker group" fi - -## Set proper permissions on docker socket chmod 666 /var/run/docker.sock - echo "Docker installation completed successfully!" '; return script; } - -} \ No newline at end of file +} diff --git a/cli/src/commands/wheels/docker/exec.cfc b/cli/src/commands/wheels/docker/exec.cfc index 106a73dc3..39905549b 100644 --- a/cli/src/commands/wheels/docker/exec.cfc +++ b/cli/src/commands/wheels/docker/exec.cfc @@ -33,6 +33,8 @@ component extends="DockerCommand" { // Check for deploy-servers file (text or json) in current directory var textConfigPath = fileSystemUtil.resolvePath("deploy-servers.txt"); var jsonConfigPath = fileSystemUtil.resolvePath("deploy-servers.json"); + var ymlConfigPath = fileSystemUtil.resolvePath("config/deploy.yml"); + var projectName = getProjectName(); // If specific servers argument is provided if (len(trim(arguments.servers))) { @@ -49,15 +51,36 @@ component extends="DockerCommand" { for (var host in hosts) { arrayAppend(serverList, { "host": trim(host), - "user": "deploy", // Default user + "user": "deploy", "port": 22, - "remoteDir": "/home/deploy/app", // Default - "imageName": "app" // Default + "remoteDir": "/home/deploy/#projectName#", + "imageName": projectName }); } } } - // Otherwise, look for default files + // 1. Look for config/deploy.yml first + else if (fileExists(ymlConfigPath)) { + var deployConfig = getDeployConfig(); + if (arrayLen(deployConfig.servers)) { + print.cyanLine("Found config/deploy.yml, loading server configuration").toConsole(); + serverList = deployConfig.servers; + + // Add defaults for missing fields + for (var s in serverList) { + if (!structKeyExists(s, "remoteDir")) { + s.remoteDir = "/home/#s.user#/#projectName#"; + } + if (!structKeyExists(s, "port")) { + s.port = 22; + } + if (!structKeyExists(s, "imageName")) { + s.imageName = projectName; + } + } + } + } + // 2. Otherwise, look for default files else if (fileExists(textConfigPath)) { print.cyanLine("Found deploy-servers.txt, loading server configuration").toConsole(); serverList = loadServersFromTextFile("deploy-servers.txt"); @@ -65,7 +88,7 @@ component extends="DockerCommand" { print.cyanLine("Found deploy-servers.json, loading server configuration").toConsole(); serverList = loadServersFromConfig("deploy-servers.json"); } else { - error("No server configuration found. Create deploy-servers.txt or deploy-servers.json in your project root."); + error("No server configuration found. Use 'wheels docker init' or create deploy-servers.txt."); } if (arrayLen(serverList) == 0) { @@ -112,7 +135,8 @@ component extends="DockerCommand" { local.host = arguments.serverConfig.host; local.user = arguments.serverConfig.user; local.port = structKeyExists(arguments.serverConfig, "port") ? arguments.serverConfig.port : 22; - local.imageName = structKeyExists(arguments.serverConfig, "imageName") ? arguments.serverConfig.imageName : "#local.user#-app"; + local.projectName = getProjectName(); + local.imageName = structKeyExists(arguments.serverConfig, "imageName") ? arguments.serverConfig.imageName : local.projectName; // 1. Check SSH Connection if (!testSSHConnection(local.host, local.user, local.port)) { diff --git a/cli/src/commands/wheels/docker/init.cfc b/cli/src/commands/wheels/docker/init.cfc index b7af5c6aa..ee3b9b1bc 100644 --- a/cli/src/commands/wheels/docker/init.cfc +++ b/cli/src/commands/wheels/docker/init.cfc @@ -10,7 +10,7 @@ * wheels docker:init --db=oracle --dbVersion=23-slim * {code} */ -component extends="../base" { +component extends="DockerCommand" { property name="detailOutput" inject="DetailOutputService@wheels-cli"; @@ -42,6 +42,32 @@ component extends="../base" { cfengine: ["lucee", "adobe"] } ); + // Welcome message + detailOutput.header("Wheels Docker Configuration"); + + // Interactive prompts for Deployment Configuration + local.appName = ask("Application Name (default: #listLast(getCWD(), '\/')#): "); + if (!len(trim(local.appName))) { + local.appName = listLast(getCWD(), '\/'); + } + + local.imageName = ask("Docker Image Name (default: #local.appName#): "); + if (!len(trim(local.imageName))) { + local.imageName = local.appName; + } + + print.line().boldCyanLine("Production Server Configuration").toConsole(); + local.serverHost = ask("Server Host/IP (e.g. 192.168.1.10): "); + local.serverUser = ""; + + if (len(trim(local.serverHost))) { + local.serverUser = ask("Server User (default: ubuntu): "); + if (!len(trim(local.serverUser))) { + local.serverUser = "ubuntu"; + } + } + print.line().toConsole(); + // Check for existing files if force is not set if (!arguments.force) { local.existingFiles = []; @@ -54,6 +80,9 @@ component extends="../base" { if (fileExists(fileSystemUtil.resolvePath(".dockerignore"))) { arrayAppend(local.existingFiles, ".dockerignore"); } + if (fileExists(fileSystemUtil.resolvePath("config/deploy.yml"))) { + arrayAppend(local.existingFiles, "config/deploy.yml"); + } if (arrayLen(local.existingFiles)) { detailOutput.line(); @@ -92,6 +121,9 @@ component extends="../base" { createDockerCompose(arguments.db, arguments.dbVersion, arguments.cfengine, arguments.cfVersion, local.appPort, arguments.production, arguments.nginx); createDockerIgnore(arguments.production); configureDatasource(arguments.db); + + // Create Deployment Config + createDeployConfig(local.appName, local.imageName, local.serverHost, local.serverUser); // Create Nginx configuration if requested if (arguments["nginx"]) { @@ -689,4 +721,29 @@ http { return; } } + + private function createDeployConfig(string appName, string imageName, string serverHost, string serverUser) { + if (!directoryExists(fileSystemUtil.resolvePath("config"))) { + directoryCreate(fileSystemUtil.resolvePath("config")); + } + + local.deployContent = "name: #arguments.appName# +image: #arguments.imageName# +servers: +"; + if (len(trim(arguments.serverHost))) { + local.deployContent &= " - host: #arguments.serverHost# + user: #arguments.serverUser# + role: production +"; + } else { + local.deployContent &= " ## - host: 192.168.1.10 + ## user: ubuntu + ## role: production +"; + } + + file action='write' file='#fileSystemUtil.resolvePath("config/deploy.yml")#' mode='777' output='#trim(local.deployContent)#'; + detailOutput.create("config/deploy.yml"); + } } \ No newline at end of file diff --git a/cli/src/commands/wheels/docker/login.cfc b/cli/src/commands/wheels/docker/login.cfc index 7badf884e..ffb9629af 100644 --- a/cli/src/commands/wheels/docker/login.cfc +++ b/cli/src/commands/wheels/docker/login.cfc @@ -13,13 +13,15 @@ component extends="DockerCommand" { * @username Registry username (required for dockerhub, ghcr, private) * @password Registry password or token (optional, will prompt if empty) * @image Image name (optional, but required for ECR/ACR to determine region/registry) + * @namespace Registry namespace/username prefix (default: same as username) * @local Execute login locally (default: true) */ function run( string registry="dockerhub", - required string username="", - required string password="", + string username="", + string password="", string image="", + string namespace="", boolean local=true ) { //ensure we are in a Wheels app @@ -39,7 +41,7 @@ component extends="DockerCommand" { } // Call loginToRegistry from base component - loginToRegistry( + var loginResult = loginToRegistry( registry=arguments.registry, image=arguments.image, username=arguments.username, @@ -47,11 +49,19 @@ component extends="DockerCommand" { isLocal=arguments.local ); + // Update arguments from interactive results for saving + arguments.username = loginResult.username; + arguments.image = loginResult.image; + if (len(trim(loginResult.registryUrl)) && !len(trim(arguments.image))) { + arguments.image = loginResult.registryUrl & "/" & getProjectName(); + } + // Save configuration for push command var config = { "registry": arguments.registry, "username": arguments.username, - "image": arguments.image + "image": arguments.image, + "namespace": arguments.namespace }; try { diff --git a/cli/src/commands/wheels/docker/logs.cfc b/cli/src/commands/wheels/docker/logs.cfc index 688ed341c..69a654d8f 100644 --- a/cli/src/commands/wheels/docker/logs.cfc +++ b/cli/src/commands/wheels/docker/logs.cfc @@ -7,7 +7,7 @@ * wheels docker logs tail=50 servers=web1.example.com * wheels docker logs service=db * wheels docker logs since=1h - * wheels docker logs --local + * wheels docker logs --remote * {code} */ component extends="DockerCommand" { @@ -18,7 +18,7 @@ component extends="DockerCommand" { * @follow Follow log output in real-time (default: false) * @service Service to show logs for: app or db (default: app) * @since Show logs since timestamp (e.g., "2023-01-01", "1h", "5m") - * @local Fetch logs from local Docker container instead of remote + * @remote Fetch logs from remote Docker container instead of local */ function run( string servers="", @@ -26,14 +26,14 @@ component extends="DockerCommand" { boolean follow=false, string service="app", string since="", - boolean local=false + boolean remote=false ) { //ensure we are in a Wheels app requireWheelsApp(getCWD()); // Reconstruct arguments for handling --key=value style arguments = reconstructArgs(arguments); - if (arguments.local) { + if (arguments.remote == false) { fetchLocalLogs(arguments.tail, arguments.follow, arguments.service, arguments.since); return; } @@ -44,6 +44,8 @@ component extends="DockerCommand" { // Check for deploy-servers file (text or json) in current directory var textConfigPath = fileSystemUtil.resolvePath("deploy-servers.txt"); var jsonConfigPath = fileSystemUtil.resolvePath("deploy-servers.json"); + var ymlConfigPath = fileSystemUtil.resolvePath("config/deploy.yml"); + var projectName = getProjectName(); // If specific servers argument is provided if (len(trim(arguments.servers))) { @@ -60,15 +62,36 @@ component extends="DockerCommand" { for (var host in hosts) { arrayAppend(serverList, { "host": trim(host), - "user": "deploy", // Default user + "user": "deploy", "port": 22, - "remoteDir": "/home/deploy/app", // Default - "imageName": "app" // Default + "remoteDir": "/home/deploy/#projectName#", + "imageName": projectName }); } } } - // Otherwise, look for default files + // 1. Look for config/deploy.yml first + else if (fileExists(ymlConfigPath)) { + var deployConfig = getDeployConfig(); + if (arrayLen(deployConfig.servers)) { + print.cyanLine("Found config/deploy.yml, loading server configuration").toConsole(); + serverList = deployConfig.servers; + + // Add defaults for missing fields + for (var s in serverList) { + if (!structKeyExists(s, "remoteDir")) { + s.remoteDir = "/home/#s.user#/#projectName#"; + } + if (!structKeyExists(s, "port")) { + s.port = 22; + } + if (!structKeyExists(s, "imageName")) { + s.imageName = projectName; + } + } + } + } + // 2. Otherwise, look for default files else if (fileExists(textConfigPath)) { print.cyanLine("Found deploy-servers.txt, loading server configuration").toConsole(); serverList = loadServersFromTextFile("deploy-servers.txt"); @@ -76,7 +99,7 @@ component extends="DockerCommand" { print.cyanLine("Found deploy-servers.json, loading server configuration").toConsole(); serverList = loadServersFromConfig("deploy-servers.json"); } else { - error("No server configuration found. Create deploy-servers.txt or deploy-servers.json in your project root."); + error("No server configuration found. Use 'wheels docker init' or create deploy-servers.txt."); } if (arrayLen(serverList) == 0) { @@ -124,7 +147,8 @@ component extends="DockerCommand" { local.host = arguments.serverConfig.host; local.user = arguments.serverConfig.user; local.port = structKeyExists(arguments.serverConfig, "port") ? arguments.serverConfig.port : 22; - local.imageName = structKeyExists(arguments.serverConfig, "imageName") ? arguments.serverConfig.imageName : "#local.user#-app"; + local.projectName = getProjectName(); + local.imageName = structKeyExists(arguments.serverConfig, "imageName") ? arguments.serverConfig.imageName : local.projectName; // 1. Check SSH Connection (skip if following to save time/output noise?) // Better to check to avoid hanging on bad connection diff --git a/cli/src/commands/wheels/docker/push.cfc b/cli/src/commands/wheels/docker/push.cfc index 7a3c677c0..98d0551c0 100644 --- a/cli/src/commands/wheels/docker/push.cfc +++ b/cli/src/commands/wheels/docker/push.cfc @@ -40,28 +40,51 @@ component extends="DockerCommand" { // Reconstruct arguments for handling --key=value style arguments = reconstructArgs(arguments); - // Load defaults from config if available + // Load defaults from config if available (prioritize deploy.yml) + var ymlConfigPath = fileSystemUtil.resolvePath("config/deploy.yml"); var configPath = fileSystemUtil.resolvePath("docker-config.json"); + var projectName = getProjectName(); + + if (fileExists(ymlConfigPath)) { + var deployConfig = getDeployConfig(); + if (structKeyExists(deployConfig, "image") && len(trim(deployConfig.image))) { + arguments.image = deployConfig.image; + } + } + if (fileExists(configPath)) { try { var config = deserializeJSON(fileRead(configPath)); if (!len(trim(arguments.registry)) && structKeyExists(config, "registry")) { arguments.registry = config.registry; - print.cyanLine("Using registry from config: #arguments.registry#").toConsole(); + print.cyanLine("Using registry from session: #arguments.registry#").toConsole(); } if (!len(trim(arguments.username)) && structKeyExists(config, "username")) { arguments.username = config.username; - print.cyanLine("Using username from config: #arguments.username#").toConsole(); + print.cyanLine("Using username from session: #arguments.username#").toConsole(); } - + + if (!len(trim(arguments.namespace)) && structKeyExists(config, "namespace")) { + arguments.namespace = config.namespace; + if (len(trim(arguments.namespace))) { + print.cyanLine("Using namespace from session: #arguments.namespace#").toConsole(); + } + } + if (!len(trim(arguments.image)) && structKeyExists(config, "image")) { arguments.image = config.image; + if (len(trim(arguments.image))) { + print.cyanLine("Using image from session: #arguments.image#").toConsole(); + } } - } catch (any e) { - // Ignore config errors - } + } catch (any e) {} + } + + // Smart Tagging logic + if (len(trim(arguments.tag)) && find(":", arguments.tag)) { + arguments.image = arguments.tag; } // Default registry to dockerhub if still empty @@ -108,9 +131,15 @@ component extends="DockerCommand" { // Get project name local.projectName = getProjectName(); + local.deployConfig = getDeployConfig(); + local.baseImageName = (structKeyExists(local.deployConfig, "image") && len(trim(local.deployConfig.image))) ? local.deployConfig.image : local.projectName; local.localImageName = local.projectName & ":latest"; - if(!checkLocalImageExists(local.projectName)){ - local.localImageName = local.projectName & "-app:latest"; + + if (!checkLocalImageExists(local.localImageName)) { + // Check if it was built with the custom image name + if (checkLocalImageExists(local.baseImageName & ":latest")) { + local.localImageName = local.baseImageName & ":latest"; + } } print.cyanLine("Project: " & local.projectName).toConsole(); @@ -163,7 +192,7 @@ component extends="DockerCommand" { // Login to registry if password provided, otherwise assume already logged in if (len(trim(arguments.password))) { - loginToRegistry( + var loginResult = loginToRegistry( registry=arguments.registry, image=local.finalImage, username=arguments.username, @@ -234,19 +263,32 @@ component extends="DockerCommand" { // Check for deploy-servers file var textConfigPath = fileSystemUtil.resolvePath("deploy-servers.txt"); var jsonConfigPath = fileSystemUtil.resolvePath("deploy-servers.json"); + var ymlConfigPath = fileSystemUtil.resolvePath("config/deploy.yml"); var allServers = []; var serversToPush = []; + var projectName = getProjectName(); + + if (len(trim(arguments.serverNumbers)) == 0 && fileExists(ymlConfigPath)) { + var deployConfig = getDeployConfig(); + if (arrayLen(deployConfig.servers)) { + print.cyanLine("Found config/deploy.yml, loading server configuration").toConsole(); + allServers = deployConfig.servers; + serversToPush = allServers; + } + } - if (fileExists(textConfigPath)) { - print.cyanLine("Found deploy-servers.txt, loading server configuration").toConsole(); - allServers = loadServersFromTextFile("deploy-servers.txt"); - serversToPush = filterServers(allServers, arguments.serverNumbers); - } else if (fileExists(jsonConfigPath)) { - print.cyanLine("Found deploy-servers.json, loading server configuration").toConsole(); - allServers = loadServersFromConfig("deploy-servers.json"); - serversToPush = filterServers(allServers, arguments.serverNumbers); - } else { - error("No server configuration found. Create deploy-servers.txt or deploy-servers.json in your project root."); + if (arrayLen(serversToPush) == 0) { + if (fileExists(textConfigPath)) { + print.cyanLine("Found deploy-servers.txt, loading server configuration").toConsole(); + allServers = loadServersFromTextFile("deploy-servers.txt"); + serversToPush = filterServers(allServers, arguments.serverNumbers); + } else if (fileExists(jsonConfigPath)) { + print.cyanLine("Found deploy-servers.json, loading server configuration").toConsole(); + allServers = loadServersFromConfig("deploy-servers.json"); + serversToPush = filterServers(allServers, arguments.serverNumbers); + } else { + error("No server configuration found. Use 'wheels docker init' or create deploy-servers.txt."); + } } if (arrayLen(serversToPush) == 0) { @@ -256,7 +298,7 @@ component extends="DockerCommand" { print.line().boldCyanLine("Pushing Docker images from #arrayLen(serversToPush)# server(s)...").toConsole(); // Push from all selected servers - pushFromServers(serversToPush, arguments.registry, arguments.image, arguments.username, arguments.password, arguments.tag); + pushFromServers(serversToPush, arguments.registry, arguments.image, arguments.username, arguments.password, arguments.tag, arguments.namespace); print.line().boldGreenLine("Push operations completed on all servers!").toConsole(); } @@ -290,7 +332,7 @@ component extends="DockerCommand" { /** * Push from multiple servers */ - private function pushFromServers(required array servers, string registry, string image, string username, string password, string tag) { + private function pushFromServers(required array servers, string registry, string image, string username, string password, string tag, string namespace="") { var successCount = 0; var failureCount = 0; @@ -301,7 +343,7 @@ component extends="DockerCommand" { print.line().boldCyanLine("---------------------------------------").toConsole(); try { - pushFromServer(serverConfig, arguments.registry, arguments.image, arguments.username, arguments.password, arguments.tag); + pushFromServer(serverConfig, arguments.registry, arguments.image, arguments.username, arguments.password, arguments.tag, arguments.namespace); successCount++; print.greenLine("Push from #serverConfig.host# completed successfully").toConsole(); } catch (any e) { @@ -321,7 +363,7 @@ component extends="DockerCommand" { /** * Push from a single server */ - private function pushFromServer(required struct serverConfig, string registry, string image, string username, string password, string tag) { + private function pushFromServer(required struct serverConfig, string registry, string image, string username, string password, string tag, string namespace="") { var local = {}; local.host = arguments.serverConfig.host; local.user = arguments.serverConfig.user; @@ -334,26 +376,41 @@ component extends="DockerCommand" { print.greenLine("SSH connection successful").toConsole(); print.cyanLine("Registry: " & arguments.registry).toConsole(); - print.cyanLine("Image: " & arguments.image).toConsole(); - // Apply additional tag if specified - if (len(trim(arguments.tag))) { - print.yellowLine("Tagging image with additional tag: " & arguments.tag).toConsole(); - local.tagCmd = "docker tag " & arguments.image & " " & arguments.tag; + // Determine final image name + local.projectName = getProjectName(); + local.finalImage = determineImageName( + arguments.registry, + arguments.image, + local.projectName, + arguments.tag, + arguments.username, + arguments.namespace + ); + + print.cyanLine("Target image: " & local.finalImage).toConsole(); + + // Tag the image on the server if it's different (e.g. if tagging project name to full name) + if (local.finalImage != arguments.image) { + print.yellowLine("Tagging image on server: " & arguments.image & " -> " & local.finalImage).toConsole(); + local.tagCmd = "docker tag " & arguments.image & " " & local.finalImage; executeRemoteCommand(local.host, local.user, local.port, local.tagCmd); - arguments.image = arguments.tag; } + // Use the final image for the rest of the operation + arguments.image = local.finalImage; + // Get login command for registry local.loginCmd = ""; if (len(trim(arguments.password))) { - local.loginCmd = loginToRegistry( + var loginResult = loginToRegistry( registry=arguments.registry, image=arguments.image, username=arguments.username, password=arguments.password, isLocal=false ); + local.loginCmd = loginResult.command; } // Execute login on remote server @@ -373,87 +430,5 @@ component extends="DockerCommand" { print.boldGreenLine("Image pushed successfully from #local.host#!").toConsole(); } - // ============================================================================= - // HELPER FUNCTIONS - // ============================================================================= - - private function loadServersFromTextFile(required string textFile) { - var filePath = fileSystemUtil.resolvePath(arguments.textFile); - var fileContent = fileRead(filePath); - var lines = listToArray(fileContent, chr(10)); - var servers = []; - - for (var line in lines) { - line = trim(line); - if (len(line) == 0 || left(line, 1) == "##") continue; - - var parts = listToArray(line, " " & chr(9), true); - if (arrayLen(parts) < 2) continue; - - arrayAppend(servers, { - "host": trim(parts[1]), - "user": trim(parts[2]), - "port": arrayLen(parts) >= 3 ? val(trim(parts[3])) : 22 - }); - } - - return servers; - } - - private function loadServersFromConfig(required string configFile) { - var configPath = fileSystemUtil.resolvePath(arguments.configFile); - var configContent = fileRead(configPath); - var config = deserializeJSON(configContent); - return config.servers; - } - - private function testSSHConnection(string host, string user, numeric port) { - print.yellowLine("Testing SSH connection to " & arguments.host & "...").toConsole(); - var result = runProcess([ - "ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", - "-p", arguments.port, arguments.user & "@" & arguments.host, "echo connected" - ]); - return (result.exitCode eq 0); - } - - private function executeRemoteCommand(string host, string user, numeric port, string cmd) { - var result = runProcess([ - "ssh", "-o", "BatchMode=yes", "-p", arguments.port, - arguments.user & "@" & arguments.host, arguments.cmd - ]); - - if (result.exitCode neq 0) { - error("Remote command failed: " & arguments.cmd); - } - return result; - } - - private function runProcess(array cmd) { - var local = {}; - local.javaCmd = createObject("java","java.util.ArrayList").init(); - for (var c in arguments.cmd) { - local.javaCmd.add(c & ""); - } - - local.pb = createObject("java","java.lang.ProcessBuilder").init(local.javaCmd); - local.pb.redirectErrorStream(true); - local.proc = local.pb.start(); - - local.isr = createObject("java","java.io.InputStreamReader").init(local.proc.getInputStream(), "UTF-8"); - local.br = createObject("java","java.io.BufferedReader").init(local.isr); - local.outputParts = []; - - while (true) { - local.line = local.br.readLine(); - if (isNull(local.line)) break; - arrayAppend(local.outputParts, local.line); - print.line(local.line).toConsole(); - } - - local.exitCode = local.proc.waitFor(); - local.output = arrayToList(local.outputParts, chr(10)); - - return { exitCode: local.exitCode, output: local.output }; - } } \ No newline at end of file diff --git a/cli/src/commands/wheels/docker/stop.cfc b/cli/src/commands/wheels/docker/stop.cfc index 8f9da0804..85e176756 100644 --- a/cli/src/commands/wheels/docker/stop.cfc +++ b/cli/src/commands/wheels/docker/stop.cfc @@ -9,7 +9,7 @@ * wheels docker stop --remote --removeContainer * {code} */ -component extends="../base" { +component extends="DockerCommand" { /** * @local Stop containers on local machine @@ -103,59 +103,9 @@ component extends="../base" { print.line(); } - /** - * Check if Docker is installed locally - */ - private function isDockerInstalled() { - try { - var result = runLocalCommand(["docker", "--version"], false); - return (result.exitCode eq 0); - } catch (any e) { - return false; - } - } - - /** - * Run a local system command - */ - private function runLocalCommand(array cmd, boolean showOutput=true) { - var local = {}; - local.javaCmd = createObject("java","java.util.ArrayList").init(); - for (var c in arguments.cmd) { - local.javaCmd.add(c & ""); - } - local.pb = createObject("java","java.lang.ProcessBuilder").init(local.javaCmd); - - // Set working directory to current directory - local.currentDir = createObject("java", "java.io.File").init(getCWD()); - local.pb.directory(local.currentDir); - - local.pb.redirectErrorStream(true); - local.proc = local.pb.start(); - - local.isr = createObject("java","java.io.InputStreamReader").init(local.proc.getInputStream(), "UTF-8"); - local.br = createObject("java","java.io.BufferedReader").init(local.isr); - local.outputParts = []; - - while (true) { - local.line = local.br.readLine(); - if (isNull(local.line)) break; - arrayAppend(local.outputParts, local.line); - if (arguments.showOutput) { - print.line(local.line).toConsole(); - } - } - - local.exitCode = local.proc.waitFor(); - local.output = arrayToList(local.outputParts, chr(10)); - - if (local.exitCode neq 0 && arguments.showOutput) { - error("Command failed with exit code: " & local.exitCode); - } + - return { exitCode: local.exitCode, output: local.output }; - } // ============================================================================= // REMOTE STOP @@ -165,23 +115,45 @@ component extends="../base" { // Check for deploy-servers file (text or json) in current directory var textConfigPath = fileSystemUtil.resolvePath("deploy-servers.txt"); var jsonConfigPath = fileSystemUtil.resolvePath("deploy-servers.json"); + var ymlConfigPath = fileSystemUtil.resolvePath("config/deploy.yml"); var allServers = []; var serversToStop = []; + var projectName = getProjectName(); - if (fileExists(textConfigPath)) { - print.cyanLine("Found deploy-servers.txt, loading server configuration").toConsole(); - allServers = loadServersFromTextFile("deploy-servers.txt"); - serversToStop = filterServers(allServers, arguments.serverNumbers); - } else if (fileExists(jsonConfigPath)) { - print.cyanLine("Found deploy-servers.json, loading server configuration").toConsole(); - allServers = loadServersFromConfig("deploy-servers.json"); - serversToStop = filterServers(allServers, arguments.serverNumbers); - } else { - error("No server configuration found. Create deploy-servers.txt or deploy-servers.json in your project root." & chr(10) & chr(10) & - "Example deploy-servers.txt:" & chr(10) & - "192.168.1.100 ubuntu 22" & chr(10) & - "production.example.com deploy" & chr(10) & chr(10) & - "Or see examples/deploy-servers.example.txt for more details."); + if (len(trim(arguments.serverNumbers)) == 0 && fileExists(ymlConfigPath)) { + var deployConfig = getDeployConfig(); + if (arrayLen(deployConfig.servers)) { + print.cyanLine("Found config/deploy.yml, loading server configuration").toConsole(); + allServers = deployConfig.servers; + + // Add defaults + for (var s in allServers) { + if (!structKeyExists(s, "remoteDir")) { + s.remoteDir = "/home/#s.user#/#projectName#"; + } + if (!structKeyExists(s, "port")) { + s.port = 22; + } + if (!structKeyExists(s, "imageName")) { + s.imageName = projectName; + } + } + serversToStop = allServers; + } + } + + if (arrayLen(serversToStop) == 0) { + if (fileExists(textConfigPath)) { + print.cyanLine("Found deploy-servers.txt, loading server configuration").toConsole(); + allServers = loadServersFromTextFile("deploy-servers.txt"); + serversToStop = filterServers(allServers, arguments.serverNumbers); + } else if (fileExists(jsonConfigPath)) { + print.cyanLine("Found deploy-servers.json, loading server configuration").toConsole(); + allServers = loadServersFromConfig("deploy-servers.json"); + serversToStop = filterServers(allServers, arguments.serverNumbers); + } else { + error("No server configuration found. Use 'wheels docker init' or create deploy-servers.txt."); + } } if (arrayLen(serversToStop) == 0) { @@ -266,8 +238,9 @@ component extends="../base" { local.host = arguments.serverConfig.host; local.user = arguments.serverConfig.user; local.port = structKeyExists(arguments.serverConfig, "port") ? arguments.serverConfig.port : 22; - local.imageName = structKeyExists(arguments.serverConfig, "imageName") ? arguments.serverConfig.imageName : "#local.user#-app"; - local.remoteDir = structKeyExists(arguments.serverConfig, "remoteDir") ? arguments.serverConfig.remoteDir : "/home/#local.user#/#local.user#-app"; + local.projectName = getProjectName(); + local.imageName = structKeyExists(arguments.serverConfig, "imageName") ? arguments.serverConfig.imageName : local.projectName; + local.remoteDir = structKeyExists(arguments.serverConfig, "remoteDir") ? arguments.serverConfig.remoteDir : "/home/#local.user#/#local.projectName#"; // Check SSH connection if (!testSSHConnection(local.host, local.user, local.port)) { @@ -340,184 +313,5 @@ component extends="../base" { print.boldGreenLine("Operations on #local.host# completed!").toConsole(); } - /** - * Load servers from simple text file - */ - private function loadServersFromTextFile(required string textFile) { - var filePath = fileSystemUtil.resolvePath(arguments.textFile); - - if (!fileExists(filePath)) { - error("Text file not found: #filePath#"); - } - - try { - var fileContent = fileRead(filePath); - var lines = listToArray(fileContent, chr(10)); - var servers = []; - - for (var lineNum = 1; lineNum <= arrayLen(lines); lineNum++) { - var line = trim(lines[lineNum]); - - // Skip empty lines and comments - if (len(line) == 0 || left(line, 1) == "##") { - continue; - } - - var parts = listToArray(line, " " & chr(9), true); - - if (arrayLen(parts) < 2) { - continue; - } - - var serverConfig = { - "host": trim(parts[1]), - "user": trim(parts[2]), - "port": arrayLen(parts) >= 3 ? val(trim(parts[3])) : 22 - }; - - var projectName = getProjectName(); - serverConfig.remoteDir = "/home/#serverConfig.user#/#projectName#"; - serverConfig.imageName = projectName; - arrayAppend(servers, serverConfig); - } - - return servers; - - } catch (any e) { - error("Error reading text file: #e.message#"); - } - } - /** - * Load servers configuration from JSON file - */ - private function loadServersFromConfig(required string configFile) { - var configPath = fileSystemUtil.resolvePath(arguments.configFile); - - if (!fileExists(configPath)) { - error("Config file not found: #configPath#"); - } - - try { - var configContent = fileRead(configPath); - var config = deserializeJSON(configContent); - - if (!structKeyExists(config, "servers") || !isArray(config.servers)) { - error("Invalid config file format. Expected { ""servers"": [ ... ] }"); - } - - var projectName = getProjectName(); - for (var i = 1; i <= arrayLen(config.servers); i++) { - var serverConfig = config.servers[i]; - if (!structKeyExists(serverConfig, "port")) { - serverConfig.port = 22; - } - if (!structKeyExists(serverConfig, "remoteDir")) { - serverConfig.remoteDir = "/home/#serverConfig.user#/#projectName#"; - } - if (!structKeyExists(serverConfig, "imageName")) { - serverConfig.imageName = projectName; - } - } - - return config.servers; - - } catch (any e) { - error("Error parsing config file: #e.message#"); - } - } - - // ============================================================================= - // HELPER FUNCTIONS - // ============================================================================= - - private function testSSHConnection(string host, string user, numeric port) { - var local = {}; - print.yellowLine("Testing SSH connection to " & arguments.host & "...").toConsole(); - local.result = runProcess([ - "ssh", - "-o", "BatchMode=yes", - "-o", "PreferredAuthentications=publickey", - "-o", "StrictHostKeyChecking=no", - "-o", "ConnectTimeout=10", - "-p", arguments.port, - arguments.user & "@" & arguments.host, - "echo connected" - ]); - return (local.result.exitCode eq 0 and findNoCase("connected", local.result.output)); - } - - private function executeRemoteCommand(string host, string user, numeric port, string cmd) { - var local = {}; - print.yellowLine("Running: ssh -p " & arguments.port & " " & arguments.user & "@" & arguments.host & " " & arguments.cmd).toConsole(); - - local.result = runProcess([ - "ssh", - "-o", "BatchMode=yes", - "-o", "PreferredAuthentications=publickey", - "-o", "StrictHostKeyChecking=no", - "-o", "ServerAliveInterval=30", - "-o", "ServerAliveCountMax=3", - "-p", arguments.port, - arguments.user & "@" & arguments.host, - arguments.cmd - ]); - - if (local.result.exitCode neq 0) { - error("Remote command failed: " & arguments.cmd); - } - - return local.result; - } - - private function runProcess(array cmd) { - var local = {}; - local.javaCmd = createObject("java","java.util.ArrayList").init(); - for (var c in arguments.cmd) { - local.javaCmd.add(c & ""); - } - - local.pb = createObject("java","java.lang.ProcessBuilder").init(local.javaCmd); - local.pb.redirectErrorStream(true); - local.proc = local.pb.start(); - - local.isr = createObject("java","java.io.InputStreamReader").init(local.proc.getInputStream(), "UTF-8"); - local.br = createObject("java","java.io.BufferedReader").init(local.isr); - local.outputParts = []; - - while (true) { - local.line = local.br.readLine(); - if (isNull(local.line)) break; - arrayAppend(local.outputParts, local.line); - print.line(local.line).toConsole(); - } - - local.exitCode = local.proc.waitFor(); - local.output = arrayToList(local.outputParts, chr(10)); - - return { exitCode: local.exitCode, output: local.output }; - } - - private function getProjectName() { - var cwd = getCWD(); - var dirName = listLast(cwd, "\/"); - dirName = lCase(dirName); - dirName = reReplace(dirName, "[^a-z0-9\-]", "-", "all"); - dirName = reReplace(dirName, "\-+", "-", "all"); - dirName = reReplace(dirName, "^\-|\-$", "", "all"); - return len(dirName) ? dirName : "wheels-app"; - } - - private function hasDockerComposeFile() { - var composeFiles = ["docker-compose.yml", "docker-compose.yaml"]; - - for (var composeFile in composeFiles) { - var composePath = getCWD() & "/" & composeFile; - if (fileExists(composePath)) { - return true; - } - } - - return false; - } } \ No newline at end of file diff --git a/docs/src/command-line-tools/commands/docker/docker-build.md b/docs/src/command-line-tools/commands/docker/docker-build.md index 69dd5a10b..35bf25865 100644 --- a/docs/src/command-line-tools/commands/docker/docker-build.md +++ b/docs/src/command-line-tools/commands/docker/docker-build.md @@ -1,594 +1,131 @@ -# Wheels Docker Build Command Guide +# wheels docker build -## Overview +Unified Docker build command for Wheels apps. Builds Docker images locally or on remote servers. -The `wheels docker build` command builds Docker images for your Wheels application without starting containers. This is useful for creating images that can be deployed later, testing build processes, or preparing images for remote deployment. - -## Prerequisites - -Before using this command, you must first initialize Docker configuration files: +## Synopsis ```bash -wheels docker init +wheels docker build [options] ``` -This will create the necessary `Dockerfile` and optionally `docker-compose.yml` files in your project directory. - -## Command Syntax - -```bash -wheels docker build [OPTIONS] -``` +## Description -## Parameters +The `wheels docker build` command handles the building of Docker images for your application. It can build images on your local machine or trigger builds on configured remote servers. -| Parameter | Type | Default | Required | Description | -|-----------|------|---------|----------|-------------| -| `--local` | boolean | `true` (if neither flag set) | No | Build Docker image on local machine | -| `--remote` | boolean | `false` | No | Build Docker image on remote server(s) | -| `--servers` | string | `""` | No | Comma-separated list of server numbers to build on (e.g., "1,3,5") - **Remote only** | -| `--tag` | string | `"project-name:latest"` | No | Custom tag for the Docker image | -| `--nocache` | boolean | `false` | No | Build without using cache (forces fresh build) | -| `--pull` | boolean | `false` | No | Always attempt to pull a newer version of the base image | +**Centralized Configuration**: +- **Source of Truth**: This command prioritizes settings from `config/deploy.yml` for server lists and image repository names. +- **Strategy Detection**: It automatically detects if you are using a `docker-compose.yml` file or a standalone `Dockerfile` and adjusts its build strategy accordingly. -## Usage Examples +## Options -### Local Build +| Option | Description | Default | +|--------|-------------|---------| +| `--local` | Build Docker image on local machine | `false` | +| `--remote` | Build Docker image on remote server(s) | `false` | +| `--servers` | Comma-separated list of server numbers to build on (e.g., "1,3,5") - for remote only | `""` | +| `--tag` | Custom tag for the Docker image (default: project-name:latest) | `""` | +| `--nocache` | Build without using cache | `false` | +| `--pull` | Always attempt to pull a newer version of the base image | `false` | -#### Basic Local Build +## Detailed Examples -Build Docker image locally with default settings: +### Local Development Builds +**Standard Build** +Builds the image using the `Dockerfile` in the current directory. Tags it with the folder name (e.g., `my-app:latest`). ```bash -wheels docker build -# or explicitly wheels docker build --local ``` -This will: -- Use `docker-compose.yml` if available, otherwise use `Dockerfile` -- Build the image with default tag (project-name:latest) -- Use Docker cache if available -- Not pull base image updates - -#### Build with Custom Tag - -```bash -wheels docker build --local --tag=myapp:v1.0.0 -``` - -#### Build Without Cache - -Force a fresh build without using cached layers: - -```bash -wheels docker build --local --nocache -``` - -#### Build with Latest Base Image - -Pull the latest version of the base image before building: - +**Force Rebuild** +If you've changed base image dependencies or want to ensure a clean build, use `--nocache` and `--pull`. ```bash -wheels docker build --local --pull +wheels docker build --local --nocache --pull ``` -#### Complete Local Build Example - -Build with custom tag, no cache, and pull latest base image: - +**Custom Tagging** +Useful when building specific versions for release. ```bash -wheels docker build --local --tag=myapp:production --nocache --pull +wheels docker build --local --tag=my-company/my-app:v2.0.0 ``` -### Remote Build - -#### Basic Remote Build - -Build on all configured remote servers: +### Remote Server Builds +**Build on All Servers** +Triggers the build process on every server listed in your `config/deploy.yml` (or legacy `deploy-servers.txt`/`deploy-servers.json`). ```bash wheels docker build --remote ``` -This will build on all servers listed in `deploy-servers.txt` or `deploy-servers.json`. - -#### Build on Specific Servers - -Build only on selected servers (by number): - +**Build on Specific Servers** +Useful if you want to update only a subset of servers (e.g., only the staging servers). ```bash -# Build on servers 1 and 3 only +# Build only on the first and third server defined in your config wheels docker build --remote --servers=1,3 - -# Build on servers 1, 2, and 5 -wheels docker build --remote --servers=1,2,5 -``` - -**Note:** Server numbers correspond to the order they appear in your configuration file (1-indexed). - -#### Remote Build with Custom Tag - -```bash -wheels docker build --remote --tag=myapp:staging -``` - -#### Remote Build Without Cache - -```bash -wheels docker build --remote --nocache -``` - -#### Remote Build with Latest Base Image - -```bash -wheels docker build --remote --pull -``` - -#### Complete Remote Build Example - -Build on specific servers with custom tag, no cache, and pull updates: - -```bash -wheels docker build --remote --servers=1,3 --tag=myapp:v2.0.0 --nocache --pull -``` - -## Server Configuration - -For remote builds, you need a server configuration file. The format is the same as used by `wheels docker deploy`. - -### Text File Format (deploy-servers.txt) - -```text -192.168.1.100 ubuntu 22 -production.example.com deploy 22 /var/www/myapp myapp-prod -staging.example.com staginguser 2222 /home/staginguser/app staging-app ``` -**Server Selection:** -- Server 1: 192.168.1.100 -- Server 2: production.example.com -- Server 3: staging.example.com - -To build only on servers 1 and 3: +**Remote Build with Cache Busting** +Forces the remote servers to pull fresh base images and ignore build cache. ```bash -wheels docker build --remote --servers=1,3 -``` - -### JSON File Format (deploy-servers.json) - -```json -{ - "servers": [ - { - "host": "192.168.1.100", - "user": "ubuntu", - "port": 22, - "remoteDir": "/var/www/myapp", - "imageName": "myapp-prod" - }, - { - "host": "production.example.com", - "user": "deploy", - "port": 22, - "remoteDir": "/home/deploy/production", - "imageName": "production-app" - }, - { - "host": "staging.example.com", - "user": "staginguser", - "port": 2222, - "remoteDir": "/home/staginguser/app", - "imageName": "staging-app" - } - ] -} +wheels docker build --remote --nocache --pull ``` ## How It Works -### Local Build Process - -1. **Check Docker Installation** - - Verifies Docker is installed and accessible - - Ensures Docker daemon is running - -2. **Detect Build Method** - - If `docker-compose.yml` exists: uses `docker compose build` - - Otherwise: uses `docker build` with Dockerfile - -3. **Build Image** - - Applies specified options (tag, nocache, pull) - - Builds image layers - - Tags the final image - -4. **Output** - - Confirms successful build - - Shows image tag - - Provides next steps (how to run/deploy) - -### Remote Build Process - -1. **Load Server Configuration** - - Reads `deploy-servers.txt` or `deploy-servers.json` - - Filters servers if `--servers` parameter is provided - -2. **For Each Server:** - - Test SSH connection - - Check if remote directory exists - - Upload source code if needed - - Detect build method (compose vs. Dockerfile) - - Execute build command remotely - - Handle sudo requirements automatically - -3. **Summary** - - Reports success/failure for each server - - Displays overall build statistics - -## Build Options Explained - -### --nocache - -Forces Docker to rebuild all layers from scratch without using the cache. - -**When to use:** -- After changing base image or dependencies -- When troubleshooting build issues -- To ensure a completely fresh build -- Before production deployments - -**Example:** -```bash -wheels docker build --nocache -``` - -### --pull - -Tells Docker to always pull the latest version of the base image specified in your Dockerfile. - -**When to use:** -- To get security updates in base images -- When you want the latest patches -- For regular maintenance builds -- Before important deployments - -**Example:** -```bash -wheels docker build --pull -``` - -### --tag - -Assigns a custom tag to the built image instead of the default `project-name:latest`. - -**Tag Format:** `name:version` - -**Common patterns:** -```bash -# Version tags -wheels docker build --tag=myapp:v1.0.0 -wheels docker build --tag=myapp:v2.1.3 - -# Environment tags -wheels docker build --tag=myapp:production -wheels docker build --tag=myapp:staging -wheels docker build --tag=myapp:dev - -# Date-based tags -wheels docker build --tag=myapp:2024-12-24 -wheels docker build --tag=myapp:20241224-1530 - -# Git-based tags -wheels docker build --tag=myapp:main -wheels docker build --tag=myapp:feature-auth -``` - -## Docker Requirements - -### Local Requirements - -- Docker Desktop (Mac/Windows) or Docker Engine (Linux) installed -- Docker daemon running -- Docker Compose (included in modern Docker installations) - -### Remote Requirements - -- SSH access to remote server -- Docker installed on remote server -- Source code present in remote directory (uploaded automatically if missing) -- Proper permissions to run Docker (user in docker group or sudo access) - -## Troubleshooting - -### Local Build Issues - -**Error: "Docker is not installed or not accessible"** - -Solution: -```bash -# Check if Docker is running -docker --version - -# Start Docker Desktop (Mac/Windows) -# Or start Docker service (Linux) -sudo systemctl start docker -``` - -**Error: "No Dockerfile or docker-compose.yml found"** - -Solution: -```bash -# Initialize Docker files first -wheels docker init -``` - -**Build fails with cache issues** - -Solution: -```bash -# Build without cache -wheels docker build --nocache -``` - -**Need latest security updates** - -Solution: -```bash -# Pull latest base image -wheels docker build --pull -``` - -### Remote Build Issues - -**Error: "SSH connection failed"** - -Solution: -- Verify SSH credentials: `ssh user@host` -- Check if SSH keys are configured -- Ensure server is accessible from your network - -**Error: "No Dockerfile found on remote server"** - -Solution: -The command automatically uploads source code if the remote directory doesn't exist. If you see this error: -- Ensure your local Dockerfile exists -- Try removing the remote directory and rebuilding -- Manually upload files with `scp` if needed - -**Error: "Remote command failed"** - -Solution: -- Check if user has Docker permissions -- Verify user is in docker group: `groups username` -- Add user to docker group: `sudo usermod -aG docker username` -- Or ensure user has sudo access - -**Build succeeds but image not found** - -Solution: -```bash -# Check images on remote server -ssh user@host "docker images" - -# Verify the tag name matches -wheels docker build --remote --tag=correct-name:latest -``` - -### Permission Issues - -**Docker permission denied** - -On remote server: -```bash -# Add user to docker group -sudo usermod -aG docker $USER - -# Apply group changes (logout/login or use) -newgrp docker - -# Or fix socket permissions (temporary) -sudo chmod 666 /var/run/docker.sock -``` - -## Build Verification - -### Local Verification - -After building locally, verify the image: - -```bash -# List all images -docker images - -# List specific project images -docker images project-name - -# Inspect the image -docker inspect project-name:latest - -# Check image size -docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" -``` - -### Remote Verification - -After building remotely, verify on the server: - -```bash -# SSH into server -ssh user@host - -# List images -docker images - -# Check specific image -docker images myapp - -# Get detailed information -docker inspect myapp:latest -``` - -## Advanced Usage - -### Build Different Versions - -```bash -# Development build -wheels docker build --tag=myapp:dev - -# Staging build with fresh base -wheels docker build --tag=myapp:staging --pull - -# Production build (complete fresh build) -wheels docker build --tag=myapp:production --nocache --pull -``` - -### Multi-Environment Remote Builds - -Assuming your `deploy-servers.txt` has: -1. dev-server (development) -2. staging-server (staging) -3. prod-server-1 (production) -4. prod-server-2 (production) - -```bash -# Build only on development -wheels docker build --remote --servers=1 --tag=myapp:dev - -# Build only on staging -wheels docker build --remote --servers=2 --tag=myapp:staging - -# Build on both production servers -wheels docker build --remote --servers=3,4 --tag=myapp:prod-v1.0 --nocache -``` - -## Best Practices - -1. **Use Version Tags for Production** - ```bash - wheels docker build --tag=myapp:v1.0.0 - ``` - Avoid using `:latest` in production environments. - -2. **Regular Cache Clearing** - Periodically rebuild without cache to prevent stale layers: - ```bash - wheels docker build --nocache - ``` - -3. **Pull Base Image Updates** - Regular security updates: - ```bash - wheels docker build --pull - ``` - -4. **Test Builds Locally First** - Always build and test locally before remote builds: - ```bash - wheels docker build --local - # Test the image - docker run -d -p 8080:8080 project-name:latest - # If successful, build remotely - wheels docker build --remote - ``` - -5. **Selective Server Builds** - Build on staging before production: - ```bash - # Build on staging first - wheels docker build --remote --servers=2 --tag=myapp:v1.0.0 - - # After validation, build on production - wheels docker build --remote --servers=3,4 --tag=myapp:v1.0.0 - ``` - -6. **Tag Naming Convention** - Use consistent tagging: - - `project:v1.0.0` - Semantic versioning - - `project:prod-20241224` - Production with date - - `project:staging` - Environment-based - - `project:feature-name` - Feature branches - -7. **Monitor Build Output** - Watch for warnings and errors during build process to catch issues early. - -8. **Keep Dockerfiles Optimized** - - Use multi-stage builds - - Minimize layer count - - Order instructions from least to most frequently changing - - Clean up in the same layer where files are created +### Local Build Strategy (`--local`) -## Build vs Deploy +1. **Prerequisite Check**: Verifies that Docker is installed and running. +2. **Compose Detection**: + * **If `docker-compose.yml` exists**: It executes `docker compose build`. This is ideal for complex apps with multiple services (app, db, redis). + * **If only `Dockerfile` exists**: It executes `docker build -t [tag] .`. +3. **Tagging**: If no custom tag is provided, it sanitizes the current directory name to create a valid Docker tag (e.g., `My Project` -> `my-project:latest`). -Understanding when to use `build` vs `deploy`: +### Remote Build Strategy (`--remote`) -### Use `wheels docker build` when: -- You want to create an image without running it -- Testing Docker configuration changes -- Preparing images for later deployment -- Building on CI/CD pipeline -- Creating multiple tagged versions -- Building on remote servers for later use +1. **Server Connection**: Reads your server config and connects via SSH. +2. **Environment Prep**: + * Checks if the remote directory exists. + * **Auto-Upload**: If the directory is missing, it automatically tars and uploads your source code to the server. +3. **Remote Execution**: + * It intelligently detects if the remote user has `docker` group privileges. + * If not, it attempts to use `sudo` for docker commands. + * It runs the appropriate build command (Compose or Standard) on the remote host. -### Use `wheels docker deploy` when: -- You want to build AND run the application -- Deploying to servers with containers running -- Full deployment workflow needed -- Starting services immediately - -### Combined Workflow: -```bash -# 1. Build the image -wheels docker build --tag=myapp:v1.0.0 --nocache - -# 2. Test locally if needed -docker run -d -p 8080:8080 myapp:v1.0.0 - -# 3. Deploy to remote servers -wheels docker deploy --remote --blueGreen -``` - -## Monitoring Build Progress - -### Local Monitoring - -Build progress is shown in real-time: -```bash -wheels docker build --local -# Output shows: -# - Layer building -# - Cache usage -# - Final image ID and size -``` - -### Remote Monitoring - -For remote builds, watch SSH output: -```bash -wheels docker build --remote -# Output shows: -# - SSH connection status -# - Source upload progress -# - Build step output from remote server -# - Success/failure summary -``` - -To monitor manually on remote server: -```bash -# In another terminal -ssh user@host "docker ps" -ssh user@host "docker images" -``` - -## Related Commands - -- [wheels docker init](docker-init.md) - Initialize Docker configuration files -- [wheels docker deploy](docker-deploy.md) - Build and deploy Docker containers -- [wheels docker push](docker-push.md) - Push Docker images to registries -- [wheels docker logs](docker-logs.md) - View container logs -- [wheels docker stop](docker-stop.md) - Stop Docker containers -- [wheels docker exec](docker-exec.md) - Execute commands in containers - ---- - -**Note**: This command is part of the Wheels CLI tool suite for Docker management. \ No newline at end of file +## Server Configuration + + This command looks for server configurations in this order of priority: + + **Option A: `config/deploy.yml` (Recommended)** + The primary source of truth for all Wheels Docker operations. + ```yaml + name: myapp + image: myuser/myapp + servers: + - host: 192.168.1.100 + user: ubuntu + role: production + ``` + + **Option B: `deploy-servers.json` (Legacy)** + ```json + { + "servers": [ + { + "host": "192.168.1.100", + "user": "ubuntu", + "port": 22, + "remoteDir": "/var/www/myapp", + "imageName": "custom-image-name" + } + ] + } + ``` + + **Option C: `deploy-servers.txt` (Legacy)** + Space-separated columns: Host, User, Port (optional). + ```text + # Host User Port + 192.168.1.100 ubuntu 22 + prod-1.example.com deploy 2202 + prod-2.example.com deploy 2202 + ``` diff --git a/docs/src/command-line-tools/commands/docker/docker-deploy.md b/docs/src/command-line-tools/commands/docker/docker-deploy.md index dd166dea0..1f9492396 100644 --- a/docs/src/command-line-tools/commands/docker/docker-deploy.md +++ b/docs/src/command-line-tools/commands/docker/docker-deploy.md @@ -1,346 +1,114 @@ -# Wheels Docker Deploy +# wheels docker deploy -## Overview +Unified Docker deployment command for Wheels apps. Deploys applications locally or to remote servers with support for Blue/Green deployment. -The `wheels docker deploy` command provides a unified interface for deploying Wheels applications using Docker. It supports both local development deployments and remote server deployments with optional Blue/Green deployment strategy for zero-downtime updates. - -## Prerequisites - -Before using this command, you must first initialize Docker configuration files: +## Synopsis ```bash -wheels docker init +wheels docker deploy [options] ``` -This will create the necessary `Dockerfile` and `docker-compose.yml` files in your project directory. +## Description -## Command Syntax +The `wheels docker deploy` command manages the deployment lifecycle of your Dockerized application. It can start containers locally for development or testing, and perform robust deployments to remote servers, including zero-downtime Blue/Green deployments. -```bash -wheels docker deploy [OPTIONS] -``` +**Centralized Configuration**: +- **Source of Truth**: This command prioritizes settings from `config/deploy.yml` for server lists and target environments. +- **Interactive Versioning**: When multiple images or tags are detected, the command provides an interactive picker to choose exactly which version to deploy. -## Parameters +## Options -| Parameter | Type | Default | Required | Description | -|-----------|------|---------|----------|-------------| -| `--local` | boolean | `true` (if neither flag set) | No | Deploy to local Docker environment | -| `--remote` | boolean | `false` | No | Deploy to remote server(s) | -| `--servers` | string | `""` | No | Server configuration file (deploy-servers.txt or deploy-servers.json) - **Remote only** | -| `--skipDockerCheck` | boolean | `false` | No | Skip Docker installation check on remote servers - **Remote only** | -| `--blueGreen` | boolean | `false` | No | Enable Blue/Green deployment strategy (zero downtime) - **Remote only** | +| Option | Description | Default | +|--------|-------------|---------| +| `--local` | Deploy to local Docker environment | `false` | +| `--remote` | Deploy to remote server(s) | `false` | +| `--environment` | Deployment environment (production, staging) - for local deployment | `production` | +| `--db` | Database to use (h2, mysql, postgres, mssql) - for local deployment | `mysql` | +| `--cfengine` | ColdFusion engine to use (lucee, adobe) - for local deployment | `lucee` | +| `--optimize` | Enable production optimizations - for local deployment | `true` | +| `--servers` | Server configuration file (defaults to `config/deploy.yml`) | `""` | +| `--skipDockerCheck` | Skip Docker installation check on remote servers | `false` | +| `--blueGreen` | Enable Blue/Green deployment strategy (zero downtime) - for remote deployment | `false` | -## Usage Examples +## Detailed Examples ### Local Deployment -#### Basic Local Deployment - -Deploy to local Docker environment: - +**Quick Start (Production Mode)** +Starts the application locally, mimicking a production environment (optimized settings, no hot-reload). ```bash -wheels docker deploy -# or explicitly wheels docker deploy --local ``` -This will: -- Use `docker-compose.yml` if available, otherwise use `Dockerfile` -- Build and start containers locally - -### Remote Deployment - -#### Basic Remote Deployment - -Deploy to remote servers using default `deploy-servers.txt` or `deploy-servers.json`: - -```bash -wheels docker deploy --remote -``` - -#### Remote Deployment with Custom Server File - -```bash -wheels docker deploy --remote --servers=production-servers.txt -``` - -or with JSON configuration: - -```bash -wheels docker deploy --remote --servers=deploy-servers.json -``` - -#### Remote Deployment with Blue/Green Strategy - -Deploy with zero downtime using Blue/Green deployment: - -```bash -wheels docker deploy --remote --blueGreen -``` - -#### Remote Deployment Skipping Docker Check - -Skip the automatic Docker installation check (useful if Docker is already installed): - +**Staging Environment** +Deploys locally with staging environment variables. ```bash -wheels docker deploy --remote --skipDockerCheck +wheels docker deploy --local --environment=staging ``` -#### Complete Remote Deployment Example - +**Custom Stack** +Deploys locally using PostgreSQL and Adobe ColdFusion. ```bash -wheels docker deploy --remote --servers=production-servers.json --blueGreen --skipDockerCheck -``` - -## Server Configuration - -For remote deployments, you need to create a server configuration file. - -### Text File Format (deploy-servers.txt) - -```text -192.168.1.100 ubuntu 22 -production.example.com deploy 22 /var/www/myapp myapp-prod -staging.example.com staginguser 2222 /home/staginguser/app staging-app +wheels docker deploy --local --db=postgres --cfengine=adobe ``` -**Format:** `host username [port] [remoteDir] [imageName]` - -- `host`: Server hostname or IP address (required) -- `username`: SSH username (required) -- `port`: SSH port (optional, default: 22) -- `remoteDir`: Remote deployment directory (optional, default: `/home/username/username-app`) -- `imageName`: Docker image name (optional, default: `username-app`) - -### JSON File Format (deploy-servers.json) - -```json -{ - "servers": [ - { - "host": "192.168.1.100", - "user": "ubuntu", - "port": 22, - "remoteDir": "/var/www/myapp", - "imageName": "myapp-prod" - }, - { - "host": "production.example.com", - "user": "deploy", - "port": 22, - "remoteDir": "/home/deploy/production", - "imageName": "production-app" - } - ] -} -``` - -## Deployment Strategies - -### Standard Deployment - -The default deployment strategy: - -1. Stops existing containers -2. Builds new Docker image -3. Starts new container -4. Brief downtime during transition +### Remote Deployment +**Standard Deployment** +Deploys to all servers defined in your configuration (starting with `config/deploy.yml`). This stops the existing container, pulls/builds the new one, and starts it. ```bash wheels docker deploy --remote ``` -### Blue/Green Deployment - -Zero-downtime deployment strategy: - -1. Builds new Docker image -2. Starts new container (green) alongside existing (blue) -3. Updates nginx proxy to route traffic to new container -4. Stops old container after successful deployment - +**Zero-Downtime Blue/Green Deployment** +Uses a Blue/Green strategy to ensure no downtime. Requires Nginx (automatically handled). ```bash wheels docker deploy --remote --blueGreen ``` -**Requirements for Blue/Green:** -- Nginx proxy container (`nginx-proxy`) -- Docker network (`web`) -- Both are automatically created if not present - -## How It Works - -### Local Deployment Process - -1. **Check for docker-compose.yml** - - If found: uses `docker compose up -d --build` - - If not found: uses standard Docker commands with `Dockerfile` - -2. **Build and Start** - - Builds Docker image from Dockerfile - - Stops existing containers (if any) - - Starts new container with proper port mapping - -3. **Output** - - Container name - - Access URL (e.g., `http://localhost:8080`) - - Useful Docker commands for monitoring - -### Remote Deployment Process - -1. **Pre-flight Checks** - - Verify SSH connection to server - - Check Docker installation (or install if missing) - - Create remote deployment directory - -2. **Upload** - - Create tarball of project source - - Upload to remote server via SCP - -3. **Build and Deploy** - - Extract source on remote server - - Build Docker image remotely - - Stop old container (if exists) - - Start new container - -4. **Post-deployment** - - Display deployment status - - Show summary for all servers - -## Docker Requirements - -### Local Requirements - -- Docker Desktop or Docker Engine installed -- Docker Compose (included in modern Docker installations) - -### Remote Requirements - -- SSH access to remote server -- Passwordless sudo access (for automatic Docker installation) -- Ubuntu/Debian or RHEL/CentOS/Fedora (for automatic Docker installation) - -If Docker is not installed on the remote server, the command will: -1. Detect the OS type -2. Install Docker automatically (requires passwordless sudo) -3. Configure Docker for the deployment user -4. Proceed with deployment - -## Troubleshooting - -### Local Deployment Issues - -**Error: "Docker is not installed or not accessible"** - -- Ensure Docker Desktop is running (Mac/Windows) -- Ensure Docker Engine is running (Linux): `sudo systemctl start docker` - -**Error: "No Dockerfile or docker-compose.yml found"** - -- Run `wheels docker init` first to create configuration files - -### Remote Deployment Issues - -**Error: "SSH connection failed"** - -- Verify server hostname/IP and SSH credentials -- Check if SSH key is properly configured -- Test manual SSH connection: `ssh user@host` - -**Error: "User requires passwordless sudo access"** - -Follow the instructions provided in the error message to configure sudoers: - +**Deploy to Specific Servers** +Uses an override server list file for deployment. ```bash -# SSH into server -ssh user@host - -# Edit sudoers file -sudo visudo - -# Add this line -username ALL=(ALL) NOPASSWD:ALL +wheels docker deploy --remote --servers=staging-servers.yml ``` -**Error: "No server configuration found"** - -- Create `deploy-servers.txt` or `deploy-servers.json` in project root -- Or specify custom file with `--servers=path/to/file` - -### Port Conflicts - -If you see port already in use errors: - +**Skip Docker Checks** +Speeds up deployment if you know Docker is already installed and configured on the remote servers. ```bash -# Check what's using the port -docker ps -lsof -i :8080 - -# Stop conflicting containers -docker stop container_name -``` - -## Monitoring Deployment - -### Local Monitoring - -```bash -# Check container status -docker ps -# or with compose -docker compose ps - -# View logs -docker logs -f container_name -# or with compose -docker compose logs -f - -# Access container shell -docker exec -it container_name /bin/bash +wheels docker deploy --remote --skipDockerCheck ``` -### Remote Monitoring - -```bash -# SSH into server -ssh user@host - -# Check containers -docker ps - -# View logs -docker logs -f container_name - -# Monitor resources -docker stats -``` +## Deployment Strategies Explained -## Security Notes +### 1. Standard Remote Deployment +This is the default strategy when `--remote` is used. +1. **Upload**: Tars and uploads your project source code to the remote server. +2. **Build/Compose**: + * If `docker-compose.yml` exists: Runs `docker compose down` followed by `docker compose up -d --build`. + * If `Dockerfile` exists: Builds the image, stops/removes the old container, and runs the new one. +3. **Downtime**: There is a short period (seconds to minutes) where the service is unavailable while the container restarts. -1. **SSH Keys**: Use SSH key authentication instead of passwords -2. **Sudo Access**: Configure minimal sudo permissions for production -3. **Firewall**: Ensure proper firewall rules are in place -4. **Docker Socket**: The deployment sets permissions on `/var/run/docker.sock` for convenience; review for production security +### 2. Blue/Green Deployment (`--blueGreen`) +This strategy is designed for zero-downtime updates. +1. **State Detection**: The script checks which "color" (Blue or Green) is currently active. +2. **Parallel Deployment**: It spins up the *new* version (e.g., Green) alongside the *old* version (Blue). +3. **Health Check**: It waits for the new container to initialize. +4. **Traffic Switch**: It updates an Nginx proxy to point traffic to the new container. +5. **Cleanup**: The old container is stopped and removed. +* **Requirement**: This strategy automatically sets up an `nginx-proxy` container on the remote server if one doesn't exist. -## Best Practices +## Troubleshooting -1. **Test Locally First**: Always test deployments locally before remote deployment -2. **Use Blue/Green for Production**: Minimize downtime with `--blueGreen` flag -3. **Version Control**: Keep `Dockerfile` and `docker-compose.yml` in version control -4. **Environment-Specific Configs**: Use different configuration files for staging/production -5. **Monitor Resources**: Keep track of Docker resource usage on remote servers -6. **Backup Data**: Always backup databases before major deployments -7. **Rollback Plan**: Keep previous images for quick rollback if needed +**"User requires passwordless sudo access"** +If the remote user is not part of the `docker` group, the CLI tries to use `sudo`. If `sudo` requires a password, deployment will fail. +* **Fix**: SSH into the server and run `sudo usermod -aG docker $USER`, then log out and back in. Or configure passwordless sudo. -## Related Commands +**"SSH connection failed"** +* **Fix**: Ensure your SSH keys are correctly loaded (`ssh-add -l`) and you can manually SSH into the server (`ssh user@host`). -- [wheels docker init](docker-init.md) - Initialize Docker configuration files -- [wheels docker build](docker-build.md) - Build Docker images -- [wheels docker push](docker-push.md) - Push Docker images to registries -- [wheels docker logs](docker-logs.md) - View container logs -- [wheels docker stop](docker-stop.md) - Stop Docker containers -- [wheels docker exec](docker-exec.md) - Execute commands in containers +**"Deployment script failed"** +* **Fix**: Run with verbose output or check the logs on the remote server. Ensure the remote server has enough disk space. ---- +## Server Configuration -**Note**: This command is part of the Wheels CLI tool suite for Docker management. \ No newline at end of file +See [wheels docker build](docker-build.md#server-configuration) for details on `deploy-servers.txt` and `deploy-servers.json`. diff --git a/docs/src/command-line-tools/commands/docker/docker-exec.md b/docs/src/command-line-tools/commands/docker/docker-exec.md index 302bea58d..00bbf0058 100644 --- a/docs/src/command-line-tools/commands/docker/docker-exec.md +++ b/docs/src/command-line-tools/commands/docker/docker-exec.md @@ -1,875 +1,96 @@ -# Wheels Docker Exec Command Guide +# wheels docker exec -## Overview +Execute commands in deployed containers. Works for both local and remote containers. -The `wheels docker exec` command allows you to execute commands inside deployed Docker containers on remote servers. This is essential for debugging, database operations, running scripts, and interactive shell access to your running containers. - ---- - -## Command Syntax - -```bash -wheels docker exec "command" [options] -``` - ---- - -## Parameters Reference - -| Parameter | Type | Required | Default | Description | -|-----------|------|----------|---------|-------------| -| `command` | String | **Yes** | - | Command to execute in the container (must be quoted if contains spaces) | -| `servers` | String | No | "" | Specific servers to execute on (comma-separated list, file path, or default config) | -| `service` | String | No | "app" | Service to execute in: `app` or `db` | -| `interactive` | Boolean | No | false | Run command interactively with TTY allocation | - ---- - -## Usage Examples - -### Basic Command Execution - -#### List Files in App Container - -Execute a simple command in the application container: - -```bash -wheels docker exec "ls -la" -``` - -#### Check Current Directory - -```bash -wheels docker exec "pwd" -``` - -#### View Environment Variables - -```bash -wheels docker exec "env" -``` - -#### Check Running Processes - -```bash -wheels docker exec "ps aux" -``` - ---- - -### File Operations - -#### View File Contents - -```bash -wheels docker exec "cat config/settings.cfm" -``` - -#### Search Files - -```bash -wheels docker exec "grep -r 'TODO' ." -``` - -#### Check Disk Usage - -```bash -wheels docker exec "df -h" -``` - -#### Find Large Files - -```bash -wheels docker exec "du -sh * | sort -h" -``` - ---- - -### Interactive Mode - -#### Interactive Shell (Bash) - -Open an interactive bash shell in the container: - -```bash -wheels docker exec "/bin/bash" --interactive -``` - -#### Interactive Shell (Sh) - -If bash is not available: - -```bash -wheels docker exec "/bin/sh" --interactive -``` - -#### CommandBox REPL - -Open CommandBox REPL for debugging: - -```bash -wheels docker exec "box repl" --interactive -``` - -#### Interactive File Editor - -Edit files interactively: - -```bash -wheels docker exec "vi config/settings.cfm" --interactive -``` - -**Note:** Press `Ctrl+C` or type `exit` to close interactive sessions. - ---- - -### Database Operations - -#### Execute MySQL Query (Non-Interactive) - -```bash -wheels docker exec "mysql -u root -ppassword -e 'SHOW DATABASES;'" service=db -``` - -#### Interactive MySQL Shell - -```bash -wheels docker exec "mysql -u root -p" service=db --interactive -``` - -#### PostgreSQL Query - -```bash -wheels docker exec "psql -U postgres -c 'SELECT version();'" service=db -``` - -#### Interactive PostgreSQL Shell - -```bash -wheels docker exec "psql -U postgres" service=db --interactive -``` - -#### Database Backup - -```bash -wheels docker exec "mysqldump -u root -ppassword mydb > /tmp/backup.sql" service=db -``` - -#### Check Database Status - -```bash -wheels docker exec "mysql -u root -ppassword -e 'SHOW STATUS;'" service=db -``` - ---- - -### Application Monitoring - -#### Tail Application Logs - -```bash -wheels docker exec "tail -f logs/application.log" -``` - -#### View Recent Errors - -```bash -wheels docker exec "tail -n 100 logs/error.log" -``` - -#### Check Memory Usage - -```bash -wheels docker exec "free -m" -``` - -#### Monitor CPU Usage - -```bash -wheels docker exec "top -bn1" -``` - -#### Check Network Connections - -```bash -wheels docker exec "netstat -tuln" -``` - ---- - -### Server Selection - -#### Execute on Specific Server - -Run command on a single server: - -```bash -wheels docker exec "ls -la" servers=web1.example.com -``` - -#### Execute on Multiple Servers - -Run command across multiple servers: - -```bash -wheels docker exec "df -h" servers=web1.example.com,web2.example.com,web3.example.com -``` - -#### Use Custom Configuration File - -```bash -wheels docker exec "pwd" servers=production-servers.json -``` - -**Note:** Interactive mode only works with a single server. - ---- - -### Service Selection - -#### Execute in Application Container (Default) - -```bash -wheels docker exec "ls /app" service=app -``` - -#### Execute in Database Container - -```bash -wheels docker exec "ls /var/lib/mysql" service=db -``` - ---- - -## Advanced Usage Examples - -### Debugging Production Issues - -#### Check Application Status - -```bash -wheels docker exec "curl -I http://localhost:8080" -``` - -#### Investigate Memory Leaks - -```bash -wheels docker exec "ps aux --sort=-%mem | head -10" -``` - -#### Analyze Log Patterns - -```bash -wheels docker exec "grep -i 'error' logs/*.log | wc -l" -``` - -#### Check Configuration Files - -```bash -wheels docker exec "cat /app/config/settings.cfm" -``` - -### Database Maintenance - -#### Check Database Size - -```bash -wheels docker exec "mysql -u root -p -e 'SELECT table_schema AS Database, ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS Size_MB FROM information_schema.tables GROUP BY table_schema;'" service=db --interactive -``` - -#### Optimize Tables - -```bash -wheels docker exec "mysql -u root -p -e 'OPTIMIZE TABLE users;'" service=db --interactive -``` - -#### Check Slow Queries - -```bash -wheels docker exec "tail -100 /var/log/mysql/slow-query.log" service=db -``` - -### Performance Analysis - -#### Generate Thread Dump - -```bash -wheels docker exec "jstack 1" -``` - -#### Check Open File Descriptors +## Synopsis ```bash -wheels docker exec "lsof | wc -l" +wheels docker exec [options] "command" ``` -#### Monitor Disk I/O - -```bash -wheels docker exec "iostat -x 1 5" -``` +## Description -### Application Management +The `wheels docker exec` command allows you to run arbitrary commands inside your running containers. It automatically locates the correct container based on the service name (e.g., `app`, `db`) and handles SSH connections for remote execution. -#### Clear Cache +**Centralized Configuration**: +- **Source of Truth**: This command prioritizes settings from `config/deploy.yml` for server lists and project names. +- **Interactive TTY**: The `--interactive` flag provides a full TTY session, allowing you to run shells, REPLs, and database clients with proper signal handling (e.g., `Ctrl+C`). -```bash -wheels docker exec "rm -rf /app/tmp/cache/*" -``` +## Options -#### Reload Application +| Option | Description | Default | +|--------|-------------|---------| +| `command` | **Required**. Command to execute in container | | +| `--servers` | Specific servers to execute on (defaults to `config/deploy.yml`) | `""` | +| `--service` | Service to execute in: `app` or `db` | `app` | +| `--interactive` | Run command interactively with full TTY support | `false` | +| `--local` | Execute in local container | `false` | -```bash -wheels docker exec "box server restart" -``` +## Detailed Examples -#### Run Migrations +### Basic Execution -```bash -wheels docker exec "box migrate up" -``` - -#### Check Box Server Status - -```bash -wheels docker exec "box server status" -``` - ---- - -## Interactive Mode Details - -### When to Use Interactive Mode - -Use `--interactive` when you need: -- Two-way communication with the command -- Input prompts (passwords, confirmations) -- Interactive shells (bash, psql, mysql) -- Text editors (vi, nano) -- REPL environments (box repl) - -### Single Server Limitation - -Interactive mode only works with **one server at a time**: - -**This will error:** -```bash -wheels docker exec "/bin/bash" servers=web1.example.com,web2.example.com --interactive -``` - -**Error:** "Cannot run interactive commands on multiple servers simultaneously." - -**Correct usage:** -```bash -wheels docker exec "/bin/bash" servers=web1.example.com --interactive -``` - -### TTY Allocation - -When `--interactive` is enabled: -- SSH allocates a TTY with `-t` flag -- Docker exec uses `-it` flags (interactive + TTY) -- Input/output streams are properly connected -- Ctrl+C, Ctrl+D, and other control sequences work correctly - ---- - -## Server Configuration - -The exec command uses the same server configuration as other Docker commands. - -### Configuration Files - -Default locations in project root: -1. `deploy-servers.txt` -2. `deploy-servers.json` - -### deploy-servers.txt Format - -```text -web1.example.com deploy 22 -web2.example.com deploy 22 -db.example.com deploy 22 -``` - -### deploy-servers.json Format - -```json -{ - "servers": [ - { - "host": "web1.example.com", - "user": "deploy", - "port": 22, - "imageName": "myapp" - }, - { - "host": "db.example.com", - "user": "deploy", - "port": 22, - "imageName": "myapp" - } - ] -} -``` - -### Direct Server Specification - -You can specify servers directly without a config file: - -```bash -wheels docker exec "ls" servers=192.168.1.100 -``` - -This uses defaults: -- User: `deploy` -- Port: `22` -- Remote Dir: `/home/deploy/app` -- Image Name: `app` - ---- - -## Container Detection - -The exec command intelligently locates the correct container. - -### Application Container Detection - -For `service=app`, searches for: -1. Exact project name match -2. `[project-name]-blue` (Blue/Green deployment) -3. `[project-name]-green` (Blue/Green deployment) -4. First container matching project name pattern - -### Database Container Detection - -For `service=db`, searches for: -1. `[project-name]-db` -2. `db` - -### Example - -If your project is named "myblog": -- App containers: `myblog`, `myblog-blue`, `myblog-green` -- DB containers: `myblog-db`, `db` - ---- - -## Command Quoting - -### When to Quote Commands - -**Always quote commands that contain:** -- Spaces -- Special characters (|, >, <, &, ;) -- Multiple arguments - -**Examples:** - -**Correct:** +**List Files** +Quickly check the file structure inside your running application container. ```bash wheels docker exec "ls -la /app" -wheels docker exec "echo 'Hello World'" -wheels docker exec "ps aux | grep java" -``` - -**Incorrect:** -```bash -wheels docker exec ls -la /app # May fail -wheels docker exec echo Hello World # May fail ``` -### Shell Pipes and Redirects - -Commands with pipes and redirects must be quoted: - -```bash -wheels docker exec "cat logs/app.log | grep ERROR" -wheels docker exec "ls -la > /tmp/files.txt" -wheels docker exec "tail -f logs/app.log & echo $!" -``` - ---- - -## Error Handling - -### Container Not Found - -**Error:** "Could not find running container for service: app" - -**Solutions:** - -1. Verify containers are running: - ```bash - wheels docker status - ``` - -2. Check container names: - ```bash - ssh user@server docker ps - ``` - -3. Specify correct service: - ```bash - wheels docker exec "ls" service=db - ``` - -### SSH Connection Failed - -**Error:** "SSH connection failed" - -**Solutions:** - -1. Test SSH manually: - ```bash - ssh user@server - ``` - -2. Verify SSH keys: - ```bash - ssh-add -l - ``` - -3. Check port configuration (default: 22) - -### Command Failed - -**Error:** "Command failed with exit code: 1" - -**Solutions:** - -1. Test command locally first: - ```bash - docker exec container-name command - ``` - -2. Check if command exists in container: - ```bash - wheels docker exec "which ls" - ``` - -3. Verify file paths: - ```bash - wheels docker exec "ls -la /app" - ``` - -### Permission Denied - -**Error:** Permission denied errors inside container - -**Solutions:** - -1. Check user permissions: - ```bash - wheels docker exec "whoami" - wheels docker exec "id" - ``` - -2. Use sudo if available: - ```bash - wheels docker exec "sudo ls /root" - ``` - -3. Check file ownership: - ```bash - wheels docker exec "ls -la /app" - ``` - ---- - -## Best Practices - -### 1. Quote All Complex Commands - -Always quote commands to avoid shell interpretation issues: - +**Check Database Connectivity** +Verify that your application container can reach the database service. ```bash -wheels docker exec "command arg1 arg2" +wheels docker exec "curl -v http://db:3306" ``` -### 2. Test Commands Locally First - -Before running on remote servers: - +**Run a CFML Script** +Execute a specific ColdFusion script or task using CommandBox. ```bash -# Test locally -docker exec container-name ls -la - -# Then run remotely -wheels docker exec "ls -la" +wheels docker exec "box task run myTask" ``` -### 3. Use Specific Server Selection - -For interactive sessions, always specify a single server: +### Interactive Sessions +**CommandBox REPL** +Start an interactive CommandBox shell to run ad-hoc CFML code. ```bash -wheels docker exec "/bin/bash" servers=web1.example.com --interactive -``` - -### 4. Specify Service for Database Commands - -Always use `service=db` for database operations: - -```bash -wheels docker exec "mysql -u root -p" service=db --interactive -``` - -### 5. Avoid Long-Running Commands on Multiple Servers - -Long commands on multiple servers can be difficult to monitor: - -```bash -# Better: Run on one server at a time -wheels docker exec "long-running-task" servers=web1.example.com -wheels docker exec "long-running-task" servers=web2.example.com -``` - -### 6. Use Non-Interactive Mode for Scripts - -For automated tasks, avoid interactive mode: - -```bash -wheels docker exec "mysql -u root -ppass -e 'SELECT COUNT(*) FROM users;'" service=db -``` - -### 7. Check Exit Codes - -The command returns Docker exec exit codes (130 = Ctrl+C is acceptable): - -```bash -wheels docker exec "test -f /app/config.cfm" && echo "File exists" -``` - -### 8. Be Careful with Destructive Commands - -Always double-check before running destructive operations: - -```bash -# Dangerous! Make sure you mean it -wheels docker exec "rm -rf /app/temp/*" -``` - -### 9. Use Absolute Paths - -Avoid confusion by using absolute paths: - -```bash -wheels docker exec "ls /app/logs" instead of "ls logs" -``` - -### 10. Handle Secrets Carefully - -Avoid putting passwords in commands when possible: - -```bash -# Bad: Password visible in command -wheels docker exec "mysql -u root -pMyPassword" service=db - -# Better: Use interactive mode -wheels docker exec "mysql -u root -p" service=db --interactive -``` - ---- - -## Common Use Cases - -### Debugging Application Issues - -```bash -# Check if application is responding -wheels docker exec "curl -I http://localhost:8080" - -# View recent errors -wheels docker exec "tail -100 logs/error.log" - -# Check Java process -wheels docker exec "ps aux | grep java" - -# View memory usage -wheels docker exec "free -m" -``` - -### Database Operations - -```bash -# Check database connection -wheels docker exec "mysql -u root -p -e 'SELECT 1;'" service=db --interactive - -# View tables -wheels docker exec "mysql -u root -p -e 'SHOW TABLES;'" service=db --interactive - -# Check database size -wheels docker exec "mysql -u root -p -e 'SELECT table_schema, SUM(data_length + index_length) / 1024 / 1024 AS size_mb FROM information_schema.tables GROUP BY table_schema;'" service=db --interactive -``` - -### File Management - -```bash -# Find configuration files -wheels docker exec "find /app -name '*.cfm'" - -# Check file permissions -wheels docker exec "ls -la /app/config" - -# Search log files -wheels docker exec "grep -r 'ERROR' /app/logs" +wheels docker exec "box repl" --interactive ``` -### Performance Monitoring - +**Database Shell** +Connect directly to the database container's shell. ```bash -# CPU usage -wheels docker exec "top -bn1 | head -20" - -# Memory usage by process -wheels docker exec "ps aux --sort=-%mem | head -10" - -# Disk usage -wheels docker exec "df -h" - -# Network stats -wheels docker exec "netstat -s" +# For MySQL +wheels docker exec "mysql -u wheels -pwheels wheels" --service=db --interactive ``` ---- - -## Integration with Other Commands - -### Check Status First - +**System Shell** +Get a bash shell inside the container for debugging. ```bash -# Check container status -wheels docker status - -# Then execute commands -wheels docker exec "ls -la" +wheels docker exec "bash" --interactive ``` -### View Logs After Execution +### Remote Execution +**Tail Logs on Remote Server** +View the live application log file on a specific remote server. ```bash -# Execute command -wheels docker exec "box migrate up" - -# Check logs for results -wheels docker logs tail=50 +wheels docker exec "tail -f logs/application.log" --servers=web1.example.com ``` -### Stop and Restart After Changes - +**Run Command on All Servers** +Execute a command across all servers defined in your configuration (non-interactive only). ```bash -# Make configuration changes -wheels docker exec "echo 'setting=value' >> /app/config/local.cfm" - -# Restart container -wheels docker stop --remote -wheels docker deploy remote +# Clear cache on all servers +wheels docker exec "box task run clearCache" ``` ---- - -## Security Considerations - -### 1. SSH Key Authentication - -Ensure SSH keys are properly configured: +**Target Specific Service** +Run a command inside the database container instead of the app container. ```bash -ssh-add ~/.ssh/id_rsa +wheels docker exec "ps aux" --service=db ``` -### 2. Avoid Hardcoded Credentials - -Don't include passwords in commands: -```bash -# Bad -wheels docker exec "mysql -u root -pSecretPassword" service=db - -# Good -wheels docker exec "mysql -u root -p" service=db --interactive -``` - -### 3. Limit Command Execution - -Only give access to trusted users who should execute commands in production. - -### 4. Audit Command History - -Keep track of executed commands for security audits. - -### 5. Use Read-Only Commands When Possible - -Prefer read-only operations for investigation: -```bash -wheels docker exec "cat /app/config/settings.cfm" # Read-only -# vs -wheels docker exec "rm /app/config/settings.cfm" # Destructive -``` - ---- - -## Troubleshooting - -### Command Hangs - -If a command hangs: -1. Press `Ctrl+C` to interrupt -2. Check if command requires input -3. Use `--interactive` if needed -4. Verify container is responsive: - ```bash - wheels docker exec "echo test" - ``` - -### Output Not Showing - -If output doesn't appear: -1. Check if command produces output: - ```bash - wheels docker exec "ls -la" - ``` -2. Redirect stderr to stdout: - ```bash - wheels docker exec "command 2>&1" - ``` - -### Interactive Mode Not Working - -If interactive mode fails: -1. Verify single server selection -2. Check TTY support: - ```bash - wheels docker exec "tty" --interactive - ``` -3. Test SSH TTY allocation: - ```bash - ssh -t user@server - ``` - ---- - -## Related Commands - -- [wheels docker init](docker-init.md) - Initialize Docker configuration files -- [wheels docker build](docker-build.md) - Build Docker images -- [wheels docker deploy](docker-deploy.md) - Build and deploy Docker containers -- [wheels docker logs](docker-logs.md) - View container logs -- [wheels docker stop](docker-stop.md) - Stop Docker containers -- [wheels docker push](docker-push.md) - Push Docker images to registries - ---- - -**Note**: This command is part of the Wheels CLI tool suite for Docker management. - -## Additional Notes +## Notes -- Commands are executed inside running containers using `docker exec` -- SSH connections use key-based authentication -- Exit code 0 indicates success, 130 indicates Ctrl+C interrupt (acceptable) -- Interactive mode requires TTY allocation on both SSH and Docker levels -- Multiple server execution is sequential, not parallel -- Commands run with the container's default user (usually root or app user) -- Working directory depends on container's WORKDIR setting -- Container must be running for exec to work -- Blue/Green deployment containers are automatically detected -- Command output is streamed in real-time \ No newline at end of file +* **Interactive Mode**: When using `--interactive`, you can only target a single server. Attempting to run interactive commands on multiple servers simultaneously will result in an error. +* **Service Discovery**: The command attempts to find the container name automatically. It looks for containers matching your project name and service (e.g., `myproject-app`, `myproject-db`). It also correctly identifies active containers in Blue/Green deployments (e.g., `myproject-app-green`). diff --git a/docs/src/command-line-tools/commands/docker/docker-init.md b/docs/src/command-line-tools/commands/docker/docker-init.md index 52daf4671..29d9d2c33 100644 --- a/docs/src/command-line-tools/commands/docker/docker-init.md +++ b/docs/src/command-line-tools/commands/docker/docker-init.md @@ -10,7 +10,9 @@ wheels docker init [options] ## Description -The `wheels docker init` command creates Docker configuration files for containerizing your Wheels application. It generates a `Dockerfile`, `docker-compose.yml`, `.dockerignore`, configures datasources in `CFConfig.json`, and optionally creates Nginx configuration for reverse proxy support. +The `wheels docker init` command creates Docker configuration files for containerizing your Wheels application. It generates a `Dockerfile`, `docker-compose.yml`, `.dockerignore`, configures datasources in `CFConfig.json`, and creates a centralized deployment configuration in `config/deploy.yml`. + +The command follows an **interactive flow** that prompts you for project-specific details, which are then used to populate your configuration files. ## Options @@ -107,13 +109,20 @@ wheels docker init --db=postgres --dbVersion=15 --cfengine=lucee --cfVersion=6 - ## What It Does -1. **Checks for existing files** (unless `--force` is used): - - Detects existing `Dockerfile`, `docker-compose.yml`, and `.dockerignore` +1. **Interactive Configuration Gathering**: + - **Application Name**: Prompts for a human-readable name for your app (defaults to folder name). + - **Docker Image Name**: Prompts for the repository name to use for builds (defaults to app name). + - **Server Host/IP**: Prompts for the production server address for deployments. + - **Server User**: Prompts for the SSH user (defaults to `ubuntu`). + - These choices are saved to `config/deploy.yml` and used by all other `wheels docker` commands. + +2. **Checks for existing files** (unless `--force` is used): + - Detects existing `Dockerfile`, `docker-compose.yml`, `.dockerignore`, and `config/deploy.yml` - Prompts before overwriting existing Docker configuration - Lists all files that will be replaced - Allows cancellation to prevent accidental overwrites -2. **Creates Dockerfile** optimized for CFML applications: +3. **Creates Dockerfile** optimized for CFML applications: - **Development mode** (default): - Hot-reload enabled - Development tools installed (curl, nano) @@ -334,6 +343,16 @@ volumes: db_data: ``` +### config/deploy.yml (New Centralized Config) +```yaml +name: myapp +image: myuser/myapp +servers: + - host: 1.2.3.4 + user: ubuntu + role: production +``` + ### nginx.conf (Generated with --nginx) ```nginx events { @@ -769,15 +788,10 @@ docker-compose up -d --build --force-recreate - Manually set `"openBrowser": false` in server.json - Rebuild containers: `docker-compose up -d --build` -## Related Commands - -- [wheels docker build](docker-build.md) - Build Docker images -- [wheels docker deploy](docker-deploy.md) - Deploy Docker containers -- [wheels docker push](docker-push.md) - Push Docker images to registries -- [wheels docker logs](docker-logs.md) - View container logs -- [wheels docker exec](docker-exec.md) - Execute commands in containers -- [wheels docker stop](docker-stop.md) - Stop Docker containers - ---- +## See Also -**Note**: This command is part of the Wheels CLI tool suite for Docker management. +- [wheels docker deploy](docker-deploy.md) - Deploy using Docker +- [wheels deploy](../deploy/deploy.md) - General deployment commands +- [CommandBox Docker Images](https://hub.docker.com/r/ortussolutions/commandbox) - Official CommandBox images +- [Docker Compose Documentation](https://docs.docker.com/compose/) - Docker Compose reference +- [Nginx Documentation](https://nginx.org/en/docs/) - Nginx configuration reference diff --git a/docs/src/command-line-tools/commands/docker/docker-login.md b/docs/src/command-line-tools/commands/docker/docker-login.md index f775a0ed7..1e094326a 100644 --- a/docs/src/command-line-tools/commands/docker/docker-login.md +++ b/docs/src/command-line-tools/commands/docker/docker-login.md @@ -1,661 +1,86 @@ -# Wheels Docker Login Command Guide +# wheels docker login -## Overview +Login to a container registry. -The `wheels docker login` command authenticates with container registries, allowing you to push and pull private images. It supports multiple popular registries including Docker Hub, Amazon ECR, Google Container Registry, Azure Container Registry, GitHub Container Registry, and private registries. - -## Command Syntax +## Synopsis ```bash -wheels docker login [OPTIONS] +wheels docker login [options] ``` -## Parameters - -| Parameter | Type | Default | Required | Description | -|-----------|------|---------|----------|-------------| -| `--registry` | string | `"dockerhub"` | No | Registry type: dockerhub, ecr, gcr, acr, ghcr, private | -| `--username` | string | `""` | Yes* | Registry username (required for dockerhub, ghcr, private) | -| `--password` | string | `""` | No | Registry password or token (will prompt if empty) | -| `--image` | string | `""` | Conditional | Image name (required for ECR/ACR to determine region/registry) | -| `--local` | boolean | `true` | No | Execute login locally | - -**Note:** Username requirements vary by registry type. For ECR and GCR, authentication is handled differently. - -## Supported Registries +## Description -| Registry | Value | Username Required | Password Handling | Additional Requirements | -|----------|-------|-------------------|-------------------|------------------------| -| Docker Hub | `dockerhub` | Yes | Prompted/provided | Docker Hub account | -| Amazon ECR | `ecr` | No* | AWS CLI | AWS credentials configured, `--image` parameter | -| Google GCR | `gcr` | No* | gcloud CLI | gcloud configured, service account | -| Azure ACR | `acr` | Yes | Prompted/provided | Azure credentials, `--image` parameter | -| GitHub CR | `ghcr` | Yes | Personal Access Token | GitHub PAT with package permissions | -| Private Registry | `private` | Yes | Prompted/provided | Custom registry URL in `--image` | +The `wheels docker login` command authenticates your Docker client with a container registry. It supports various registry types including Docker Hub, AWS ECR, Google GCR, Azure ACR, and GitHub GHCR. -*Uses CLI tools for authentication +**Interactive Features**: +- **Username Recovery**: If you omit the `--username` argument, the command will proactively prompt you for it. +- **Secure Input**: Passwords and tokens are masked during entry. +- **Ordered Prompts**: For private and ACR registries, the command first asks for the Registry URL to ensure proper host resolution. -## Usage Examples +## Options -### Docker Hub Login +| Option | Description | Default | +|--------|-------------|---------| +| `--registry` | Registry type: `dockerhub`, `ecr`, `gcr`, `acr`, `ghcr`, `private` | `dockerhub` | +| `--username` | Registry username (required for dockerhub, ghcr, private) | `""` | +| `--password` | Registry password or token (optional, will prompt if empty) | `""` | +| `--image` | Image name (optional, but required for ECR/ACR to determine region/registry) | `""` | +| `--local` | Execute login locally | `true` | -#### Basic Docker Hub Login (Interactive) - -Login to Docker Hub with password prompt: +## Detailed Examples +### Docker Hub +The default registry. Requires your Docker Hub username and password/token. ```bash -wheels docker login --registry=dockerhub --username=myusername -``` - -You'll be prompted to enter your password securely. +# Prompts for password +wheels docker login --registry=dockerhub --username=myuser -#### Docker Hub Login with Password - -Provide password directly (less secure, not recommended for production): - -```bash -wheels docker login --registry=dockerhub --username=myusername --password=mypassword +# With password provided (use with caution in scripts) +wheels docker login --registry=dockerhub --username=myuser --password=mytoken ``` -#### Docker Hub Login with Access Token - -Use a personal access token instead of password: - +### AWS Elastic Container Registry (ECR) +Authenticates with AWS ECR. Requires the full image repository URI to determine the region and registry ID. ```bash -wheels docker login --registry=dockerhub --username=myusername --password=dckr_pat_abc123xyz +wheels docker login --registry=ecr --image=123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:latest ``` -### Amazon ECR (Elastic Container Registry) - -#### ECR Login with Image URL - -The `--image` parameter is required to determine the AWS region and registry: - +### GitHub Container Registry (GHCR) +Authenticates with GitHub's container registry. Use your GitHub username and a Personal Access Token (PAT) with `read:packages` scope. ```bash -wheels docker login --registry=ecr --image=123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest +wheels docker login --registry=ghcr --username=mygithubuser ``` -#### ECR Login with Different Regions - -```bash -# US East region -wheels docker login --registry=ecr --image=123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp - -# EU West region -wheels docker login --registry=ecr --image=123456789012.dkr.ecr.eu-west-1.amazonaws.com/myapp - -# Asia Pacific region -wheels docker login --registry=ecr --image=123456789012.dkr.ecr.ap-southeast-1.amazonaws.com/myapp -``` - -**Prerequisites for ECR:** -- AWS CLI installed and configured -- AWS credentials set up (via `aws configure` or environment variables) -- Proper IAM permissions for ECR - ### Google Container Registry (GCR) - -#### GCR Login - -```bash -wheels docker login --registry=gcr --image=gcr.io/my-project-id/myapp:latest -``` - -**GCR Authentication Methods:** - -Different authentication formats supported: +Authenticates with GCR. Requires the image path to determine the registry host (e.g., `gcr.io`, `us.gcr.io`). ```bash -# Standard GCR -wheels docker login --registry=gcr --image=gcr.io/project-id/image - -# Regional GCR (US) -wheels docker login --registry=gcr --image=us.gcr.io/project-id/image - -# Regional GCR (EU) -wheels docker login --registry=gcr --image=eu.gcr.io/project-id/image - -# Regional GCR (Asia) -wheels docker login --registry=gcr --image=asia.gcr.io/project-id/image +wheels docker login --registry=gcr --image=gcr.io/my-project/my-app ``` -**Prerequisites for GCR:** -- gcloud CLI installed and configured -- Service account with Container Registry permissions -- gcloud authentication set up (`gcloud auth configure-docker`) - ### Azure Container Registry (ACR) - -#### ACR Login with Username - -```bash -wheels docker login --registry=acr --username=myregistry --image=myregistry.azurecr.io/myapp:latest -``` - -#### ACR Login with Service Principal - -```bash -wheels docker login --registry=acr --username=service-principal-id --password=service-principal-password --image=myregistry.azurecr.io/myapp -``` - -**Prerequisites for ACR:** -- Azure CLI installed (optional but recommended) -- Azure Container Registry credentials -- Registry URL in the format: `registryname.azurecr.io` - -### GitHub Container Registry (GHCR) - -#### GHCR Login with Personal Access Token - -```bash -wheels docker login --registry=ghcr --username=githubusername --password=ghp_yourpersonalaccesstoken -``` - -#### GHCR Login (Interactive) - -```bash -wheels docker login --registry=ghcr --username=githubusername -# Password will be prompted -``` - -**Creating a GitHub Personal Access Token:** -1. Go to GitHub Settings → Developer settings → Personal access tokens -2. Generate new token (classic) -3. Select scopes: `write:packages`, `read:packages`, `delete:packages` -4. Copy the token (starts with `ghp_`) - -**GHCR Image Format:** +Authenticates with Azure ACR. Requires the login server address. ```bash -ghcr.io/username/repository-name:tag -ghcr.io/organization/repository-name:tag +wheels docker login --registry=acr --image=myregistry.azurecr.io/my-app ``` -### Private Registry Login - -#### Private Registry Login - -```bash -wheels docker login --registry=private --username=registryuser --image=registry.company.com/myapp:latest -``` - -#### Private Registry with Custom Port - -```bash -wheels docker login --registry=private --username=registryuser --image=registry.company.com:5000/myapp:latest -``` - -#### Self-Hosted Registry - +### Private Registry +For self-hosted or other private registries. If you don't provide an image or URL via arguments, you will be prompted in this order: **Registry URL** -> **Username** -> **Password**. ```bash -wheels docker login --registry=private --username=admin --password=secret --image=localhost:5000/myapp +wheels docker login --registry=private ``` -## Configuration File +## Configuration Storage -After successful login, credentials are saved to `docker-config.json` in your project root: +Successful logins save the registry configuration to `docker-config.json` in your project root. This configuration is used by `wheels docker push` to automatically authenticate during push operations. +**Example `docker-config.json`:** ```json { - "registry": "dockerhub", - "username": "myusername", - "image": "" + "registry": "ecr", + "image": "123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:latest", + "username": "AWS", + "namespace": "myorg" } ``` -This configuration is used by the `wheels docker push` command for automatic registry detection. - -**Note:** The password is NOT saved in this file. Docker stores credentials securely in the system keychain or credentials store. - -## How It Works - -### Authentication Flow - -1. **Validate Parameters** - - Check if registry type is supported - - Verify required parameters for the registry type - - Validate image format if provided - -2. **Docker Installation Check** - - Verify Docker is installed and running locally - - Ensure Docker daemon is accessible - -3. **Registry-Specific Authentication** - - **Docker Hub/GHCR/Private:** Uses `docker login` with username/password - - **ECR:** Uses AWS CLI to get temporary credentials - - **GCR:** Uses gcloud helper for authentication - - **ACR:** Uses Azure credentials with `docker login` - -4. **Save Configuration** - - Stores registry settings in `docker-config.json` - - Does not store sensitive credentials in the file - - Used for subsequent push operations - -### Where Credentials Are Stored - -Docker stores credentials securely in: - -- **macOS:** macOS Keychain -- **Windows:** Windows Credential Manager -- **Linux:** - - `~/.docker/config.json` (with credential helpers) - - Or encrypted using `pass`, `secretservice`, or other helpers - -## Registry-Specific Setup - -### Docker Hub Setup - -1. **Create Docker Hub Account** - - Visit https://hub.docker.com - - Sign up for free account - -2. **Create Access Token (Recommended)** - ```bash - # Login to Docker Hub web interface - # Account Settings → Security → New Access Token - # Use token instead of password - wheels docker login --registry=dockerhub --username=myuser --password=dckr_pat_token - ``` - -### Amazon ECR Setup - -1. **Install AWS CLI** - ```bash - # macOS - brew install awscli - - # Linux - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" - unzip awscliv2.zip - sudo ./aws/install - - # Windows - # Download MSI installer from AWS website - ``` - -2. **Configure AWS Credentials** - ```bash - aws configure - # Enter: AWS Access Key ID - # Enter: AWS Secret Access Key - # Enter: Default region (e.g., us-east-1) - # Enter: Default output format (json) - ``` - -3. **Verify ECR Access** - ```bash - aws ecr describe-repositories - ``` - -4. **Login to ECR** - ```bash - wheels docker login --registry=ecr --image=123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp - ``` - -### Google Container Registry Setup - -1. **Install gcloud CLI** - ```bash - # macOS - brew install --cask google-cloud-sdk - - # Linux - curl https://sdk.cloud.google.com | bash - exec -l $SHELL - - # Windows - # Download installer from Google Cloud - ``` - -2. **Authenticate with gcloud** - ```bash - gcloud auth login - gcloud config set project YOUR-PROJECT-ID - ``` - -3. **Configure Docker for GCR** - ```bash - gcloud auth configure-docker - ``` - -4. **Login via Wheels** - ```bash - wheels docker login --registry=gcr --image=gcr.io/my-project/myapp - ``` - -### Azure Container Registry Setup - -1. **Install Azure CLI (Optional)** - ```bash - # macOS - brew install azure-cli - - # Linux - curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash - - # Windows - # Download MSI from Microsoft - ``` - -2. **Get ACR Credentials** - ```bash - # Via Azure Portal - # Container Registry → Access keys → Enable Admin user - # Copy Username and Password - - # Or via CLI - az acr credential show --name myregistry - ``` - -3. **Login** - ```bash - wheels docker login --registry=acr --username=myregistry --image=myregistry.azurecr.io/myapp - ``` - -### GitHub Container Registry Setup - -1. **Create Personal Access Token** - - GitHub → Settings → Developer settings → Personal access tokens - - Generate new token (classic) - - Select scopes: `write:packages`, `read:packages` - - Copy token (starts with `ghp_`) - -2. **Login to GHCR** - ```bash - wheels docker login --registry=ghcr --username=your-github-username --password=ghp_yourtoken - ``` - -3. **Verify Authentication** - ```bash - docker pull ghcr.io/your-username/your-image:latest - ``` - -## Docker Requirements - -### Local Requirements - -- Docker Desktop (Mac/Windows) or Docker Engine (Linux) installed and running -- Docker daemon accessible -- Internet connection for registry authentication - -### Command-Line Tools (Registry-Specific) - -| Registry | Required Tool | Installation | -|----------|--------------|--------------| -| Docker Hub | Docker CLI | Included with Docker | -| ECR | AWS CLI | `brew install awscli` or download from AWS | -| GCR | gcloud CLI | `brew install google-cloud-sdk` or download | -| ACR | Azure CLI (optional) | `brew install azure-cli` or download | -| GHCR | Docker CLI | Included with Docker | -| Private | Docker CLI | Included with Docker | - -## Troubleshooting - -### Common Issues - -#### Docker Not Running - -**Error:** "Docker is not installed or not accessible" - -**Solution:** -```bash -# Check Docker status -docker --version -docker ps - -# Start Docker Desktop (Mac/Windows) -# Or start Docker daemon (Linux) -sudo systemctl start docker -``` - -#### Authentication Failed - -**Error:** "Login failed" or "unauthorized" - -**Solutions:** - -For **Docker Hub:** -```bash -# Verify credentials -# Try password reset at hub.docker.com -# Use access token instead of password -wheels docker login --registry=dockerhub --username=user --password=dckr_pat_token -``` - -For **ECR:** -```bash -# Verify AWS credentials -aws sts get-caller-identity - -# Check ECR permissions -aws ecr describe-repositories - -# Reconfigure AWS -aws configure -``` - -For **GCR:** -```bash -# Verify gcloud authentication -gcloud auth list - -# Re-authenticate -gcloud auth login -gcloud auth configure-docker -``` - -For **GHCR:** -```bash -# Verify token has correct permissions -# Regenerate token with write:packages scope -# Ensure username is correct (case-sensitive) -``` - -#### Invalid Image Format - -**Error:** "Invalid image format" or "Could not parse registry URL" - -**Solution:** -```bash -# Ensure correct format for each registry: - -# Docker Hub ---image=username/repository:tag - -# ECR ---image=123456789012.dkr.ecr.region.amazonaws.com/repository:tag - -# GCR ---image=gcr.io/project-id/repository:tag - -# ACR ---image=registryname.azurecr.io/repository:tag - -# GHCR ---image=ghcr.io/username/repository:tag -``` - -#### ECR Region Issues - -**Error:** "No basic auth credentials" for ECR - -**Solution:** -```bash -# Ensure image URL includes correct region -wheels docker login --registry=ecr --image=123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp - -# Verify AWS region configuration -aws configure get region - -# Set region explicitly -export AWS_REGION=us-east-1 -``` - -#### Permission Denied - -**Error:** "Permission denied" when running Docker commands - -**Solution:** -```bash -# Add user to docker group (Linux) -sudo usermod -aG docker $USER -newgrp docker - -# Restart Docker Desktop (Mac/Windows) -``` - -### Debug Mode - -To troubleshoot authentication issues: - -```bash -# Check Docker credentials -docker-credential-desktop list # macOS -docker-credential-wincred list # Windows -cat ~/.docker/config.json # Linux - -# Test manual login -docker login registry.example.com - -# Verify stored credentials -docker logout registry.example.com -docker login registry.example.com -``` - -## Security Best Practices - -### 1. Use Access Tokens Instead of Passwords - -```bash -# Docker Hub - create access token -wheels docker login --registry=dockerhub --username=user --password=dckr_pat_abc123 - -# GitHub - use personal access token -wheels docker login --registry=ghcr --username=user --password=ghp_token123 -``` - -### 2. Limit Token Permissions - -- Grant minimum required permissions -- Use read-only tokens when only pulling images -- Use tokens for authentication - -### 3. Rotate Credentials Regularly - -```bash -# Logout and login with new credentials -docker logout -wheels docker login --registry=dockerhub --username=user -``` - -### 4. Avoid Hardcoding Passwords - -```bash -# Bad - password in command -wheels docker login --registry=dockerhub --username=user --password=secret123 - -# Good - let it prompt -wheels docker login --registry=dockerhub --username=user - -# Better - use environment variable -export DOCKER_PASSWORD="secret123" -wheels docker login --registry=dockerhub --username=user --password=$DOCKER_PASSWORD -``` - -### 5. Never Hardcode Credentials - -```bash -# Never commit credentials to version control -echo "docker-config.json" >> .gitignore -``` - -### 6. Use Service Accounts for Production - -- **ECR:** Use IAM roles instead of access keys -- **GCR:** Use service accounts with limited permissions -- **ACR:** Use service principals with specific roles -- **GHCR:** Use machine users or bot accounts - -### 7. Enable Two-Factor Authentication - -Enable 2FA on registry accounts where supported: -- Docker Hub: Account Settings → Security -- GitHub: Settings → Password and authentication - -### 8. Audit Access Logs - -Regularly review access logs: -```bash -# AWS CloudTrail for ECR -# Azure Monitor for ACR -# GitHub audit log for GHCR -``` - -## Multiple Registry Workflow - -Working with multiple registries: - -```bash -# Login to Docker Hub -wheels docker login --registry=dockerhub --username=user1 - -# Login to GitHub Container Registry -wheels docker login --registry=ghcr --username=user2 - -# Login to ECR -wheels docker login --registry=ecr --image=123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp - -# Build once -wheels docker build --tag=myapp:v1.0.0 - -# Push to multiple registries -docker tag myapp:v1.0.0 user1/myapp:v1.0.0 -docker push user1/myapp:v1.0.0 - -docker tag myapp:v1.0.0 ghcr.io/user2/myapp:v1.0.0 -docker push ghcr.io/user2/myapp:v1.0.0 - -docker tag myapp:v1.0.0 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:v1.0.0 -docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:v1.0.0 -``` - -## Verification - -### Verify Successful Login - -```bash -# Check Docker config -cat ~/.docker/config.json - -# Should show authenticated registries -{ - "auths": { - "https://index.docker.io/v1/": {}, - "ghcr.io": {}, - "123456789012.dkr.ecr.us-east-1.amazonaws.com": {} - } -} -``` - -### Test Authentication - -```bash -# Try pulling a private image -docker pull your-registry/private-image:latest - -# Or push a test image -docker tag hello-world your-registry/test:latest -docker push your-registry/test:latest -``` - -## Related Commands - -- [wheels docker init](docker-init.md) - Initialize Docker configuration files -- [wheels docker build](docker-build.md) - Build Docker images -- [wheels docker deploy](docker-deploy.md) - Build and deploy Docker containers -- [wheels docker push](docker-push.md) - Push Docker images to registries -- [wheels docker logs](docker-logs.md) - View container logs -- [wheels docker exec](docker-exec.md) - Execute commands in containers -- [wheels docker stop](docker-stop.md) - Stop Docker containers - ---- - -**Note**: This command is part of the Wheels CLI tool suite for Docker management. \ No newline at end of file +**Security Note**: Passwords are **not** stored in `docker-config.json`. Only registry type, username, and image references are saved. You may be prompted for a password again during push operations if your session expires. diff --git a/docs/src/command-line-tools/commands/docker/docker-logs.md b/docs/src/command-line-tools/commands/docker/docker-logs.md index 2cfe1623e..4d973932a 100644 --- a/docs/src/command-line-tools/commands/docker/docker-logs.md +++ b/docs/src/command-line-tools/commands/docker/docker-logs.md @@ -10,28 +10,39 @@ wheels docker logs [options] ## Description -The `wheels docker logs` command fetches and displays logs from running containers. It supports fetching logs from specific services (app, db) and can stream logs in real-time. It abstracts away the complexity of finding the correct container ID, especially in multi-server or Blue/Green environments. +The `wheels docker logs` command fetches and displays logs from running containers. It abstracts away the complexity of finding the correct container ID, especially in multi-server or Blue/Green environments. + +**Centralized Configuration**: +- **Source of Truth**: This command prioritizes settings from `config/deploy.yml` for server lists and project names. +- **Local vs Remote**: You can explicitly switch between viewing local container logs and remote server logs using the `--local` flag. ## Options | Option | Description | Default | |--------|-------------|---------| -| `--servers` | Specific servers to check (comma-separated list of hosts or file path) | `""` | +| `--servers` | Specific servers to check (defaults to `config/deploy.yml`) | `""` | +| `--local` | Fetch logs from local Docker environment | `false` | | `--tail` | Number of lines to show | `100` | | `--follow` | Follow log output in real-time | `false` | | `--service` | Service to show logs for: `app` or `db` | `app` | -| `--since` | Show logs since timestamp (e.g., "2023-01-01", "1h", "5m") | `""` | +| `--since` | Show logs since timestamp | `""` | ## Detailed Examples ### Basic Usage -**View Recent Logs** -Fetches the last 100 lines of logs from the application container on all configured servers. +**View Recent Logs (Remote)** +Fetches the last 100 lines of logs from the application container on all configured remote servers. ```bash wheels docker logs ``` +**View Local Logs** +Fetches logs from the local Docker environment. +```bash +wheels docker logs --local +``` + **View Database Logs** Fetches logs from the database service container. ```bash @@ -85,16 +96,3 @@ wheels docker logs --service=db --tail=100 * **Service Discovery**: Automatically finds the correct container, handling Blue/Green deployment naming (e.g., it knows to look at `myapp-green` if that's the active container). * **SSH**: Uses your local SSH configuration. Ensure you have access to the servers. - -## Related Commands - -- [wheels docker init](docker-init.md) - Initialize Docker configuration files -- [wheels docker build](docker-build.md) - Build Docker images -- [wheels docker deploy](docker-deploy.md) - Build and deploy Docker containers -- [wheels docker exec](docker-exec.md) - Execute commands in containers -- [wheels docker stop](docker-stop.md) - Stop Docker containers -- [wheels docker push](docker-push.md) - Push Docker images to registries - ---- - -**Note**: This command is part of the Wheels CLI tool suite for Docker management. diff --git a/docs/src/command-line-tools/commands/docker/docker-push.md b/docs/src/command-line-tools/commands/docker/docker-push.md index 3656f41c4..1d33b0d77 100644 --- a/docs/src/command-line-tools/commands/docker/docker-push.md +++ b/docs/src/command-line-tools/commands/docker/docker-push.md @@ -1,816 +1,94 @@ -# Wheels Docker Push Command Guide +# wheels docker push -## Overview +Push Docker images to container registries. -The `wheels docker push` command uploads Docker images to container registries. It works in conjunction with `wheels docker login` to authenticate and push images to Docker Hub, Amazon ECR, Google Container Registry, Azure Container Registry, GitHub Container Registry, and private registries. - -## Prerequisites - -Before pushing images, you must: - -1. **Authenticate with the registry:** - ```bash - wheels docker login --registry=dockerhub --username=myuser - ``` - -2. **Have a built image:** - ```bash - wheels docker build --local - ``` - Or use the `--build` flag with the push command to build automatically. - -## Command Syntax +## Synopsis ```bash -wheels docker push [OPTIONS] -``` - -## Parameters - -| Parameter | Type | Default | Required | Description | -|-----------|------|---------|----------|-------------| -| `--local` | boolean | `true` (if neither flag set) | No | Push image from local machine | -| `--remote` | boolean | `false` | No | Push image from remote server(s) | -| `--servers` | string | `""` | No | Comma-separated list of server numbers to push from (e.g., "1,3,5") - **Remote only** | -| `--registry` | string | `"dockerhub"` | No | Registry type: dockerhub, ecr, gcr, acr, ghcr, private | -| `--image` | string | `""` | No | Full image name with registry path (auto-detected if not specified) | -| `--username` | string | `""` | Conditional* | Registry username (required for dockerhub, ghcr, private) | -| `--password` | string | `""` | No | Registry password or token (uses existing credentials if not provided) | -| `--tag` | string | `"latest"` | No | Tag/version to apply (e.g., v1.0.0, latest) | -| `--build` | boolean | `false` | No | Build the image before pushing | -| `--namespace` | string | `""` | No | Registry namespace/username prefix | - -**Note:** Username is loaded from `docker-config.json` if available (created by `wheels docker login`). - -## Configuration File - -After running `wheels docker login`, credentials are saved to `docker-config.json`: - -```json -{ - "registry": "dockerhub", - "username": "myusername", - "image": "" -} +wheels docker push [options] ``` -The push command automatically reads this configuration, so you don't need to specify registry and username again. - -## Usage Examples +## Description -### Local Push +The `wheels docker push` command uploads your Docker images to a container registry (like Docker Hub, ECR, etc.). It can push images from your local machine or trigger remote servers to push their built images. -#### Basic Local Push (Using Saved Configuration) +**Smart Tagging & Config**: +- **Source of Truth**: Automatically reads settings from `config/deploy.yml` if available. +- **Smart Tagging**: Intelligently handles custom image names and version suffixes. +- **Interactive Registry Detection**: Prompts for Registry URLs if they cannot be found in configuration for private/ACR types. -After logging in with `wheels docker login`: +## Options -```bash -wheels docker push -# or explicitly -wheels docker push --local -``` +| Option | Description | Default | +|--------|-------------|---------| +| `--local` | Push image from local machine | `false` | +| `--remote` | Push image from remote server(s) | `false` | +| `--servers` | Comma-separated list of server numbers to push from (e.g., "1,3,5") - for remote only | `""` | +| `--registry` | Registry type: `dockerhub`, `ecr`, `gcr`, `acr`, `ghcr`, `private` | `dockerhub` | +| `--image` | Full image name with registry path (optional - auto-detected if not specified) | `""` | +| `--username` | Registry username | `""` | +| `--password` | Registry password or token (leave empty to prompt) | `""` | +| `--tag` | Tag/version to apply (e.g., v1.0.0, latest) | `latest` | +| `--build` | Build the image before pushing | `false` | +| `--namespace` | Registry namespace/username prefix | `""` | -This uses the registry and username from `docker-config.json`. +## Detailed Examples -#### Push with Explicit Registry and Username +### Local Push Workflows +**Push to Docker Hub** +Pushes your image to Docker Hub under your username. ```bash +# Assumes you are logged in or will be prompted for password wheels docker push --local --registry=dockerhub --username=myuser ``` -#### Push with Custom Tag - +**Push a Specific Version** +Tags the image as `v1.0.0` and pushes it. ```bash -# Push as version 1.0.0 -wheels docker push --local --tag=v1.0.0 - -# Push as latest (default) -wheels docker push --local --tag=latest - -# Push with semantic versioning -wheels docker push --local --tag=v2.1.3 -``` - -#### Build and Push in One Command - -```bash -wheels docker push --local --build -``` - -This will: -1. Build the Docker image -2. Push it to the registry - -#### Push with Full Image Name - -```bash -wheels docker push --local --image=myusername/myapp:v1.0.0 -``` - -#### Push to Docker Hub with Namespace - -```bash -wheels docker push --local --registry=dockerhub --username=myuser --namespace=myorganization -``` - -Creates image: `myorganization/projectname:latest` - -### Registry-Specific Examples - -#### Docker Hub - -```bash -# Using saved credentials -wheels docker push --local - -# Explicit credentials -wheels docker push --local --registry=dockerhub --username=myuser --password=mytoken - -# With custom tag wheels docker push --local --registry=dockerhub --username=myuser --tag=v1.0.0 - -# Build and push -wheels docker push --local --registry=dockerhub --username=myuser --build -``` - -#### Amazon ECR - -```bash -# ECR requires full image path -wheels docker push --local --registry=ecr --image=123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest - -# With custom tag -wheels docker push --local --registry=ecr --image=123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:v1.0.0 - -# Build and push to ECR -wheels docker push --local --registry=ecr --image=123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest --build -``` - -#### Google Container Registry - -```bash -# GCR push -wheels docker push --local --registry=gcr --image=gcr.io/my-project-id/myapp:latest - -# Regional GCR -wheels docker push --local --registry=gcr --image=us.gcr.io/my-project-id/myapp:latest - -# With tag -wheels docker push --local --registry=gcr --image=gcr.io/my-project-id/myapp:v1.0.0 -``` - -#### Azure Container Registry - -```bash -# ACR push -wheels docker push --local --registry=acr --username=myregistry --image=myregistry.azurecr.io/myapp:latest - -# With custom tag -wheels docker push --local --registry=acr --username=myregistry --image=myregistry.azurecr.io/myapp:v1.0.0 ``` -#### GitHub Container Registry - +**Build and Push** +Ensures you are pushing the absolute latest code by building first. ```bash -# GHCR push -wheels docker push --local --registry=ghcr --username=githubuser --image=ghcr.io/githubuser/myapp:latest - -# Organization repository -wheels docker push --local --registry=ghcr --username=githubuser --image=ghcr.io/myorg/myapp:latest - -# With tag -wheels docker push --local --registry=ghcr --username=githubuser --tag=v1.0.0 +wheels docker push --local --registry=dockerhub --username=myuser --build ``` -#### Private Registry - +**Push to AWS ECR** +Pushes to a private ECR repository. ```bash -# Private registry push -wheels docker push --local --registry=private --username=registryuser --image=registry.company.com/myapp:latest - -# With custom port -wheels docker push --local --registry=private --username=registryuser --image=registry.company.com:5000/myapp:latest +wheels docker push --local --registry=ecr --image=123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:latest ``` -### Remote Push - -#### Basic Remote Push - -Push from all configured remote servers: +### Remote Push Workflows +**Trigger Remote Push** +Instructs all remote servers to push their built images to the registry. This is useful if your remote servers build the images (e.g., during deployment) and you want to archive those exact artifacts. ```bash wheels docker push --remote --registry=dockerhub --username=myuser ``` -#### Push from Specific Servers - -```bash -# Push from servers 1 and 3 only -wheels docker push --remote --servers=1,3 --registry=dockerhub --username=myuser - -# Push from server 2 -wheels docker push --remote --servers=2 --registry=dockerhub --username=myuser -``` - -#### Remote Push with Authentication - -```bash -wheels docker push --remote --registry=dockerhub --username=myuser --password=mytoken -``` - -#### Remote Push with Custom Tag - -```bash -wheels docker push --remote --registry=dockerhub --username=myuser --tag=v1.0.0 -``` - -#### Remote Push to ECR - -```bash -wheels docker push --remote --registry=ecr --image=123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest -``` - -## How It Works - -### Local Push Process - -1. **Load Configuration** - - Reads `docker-config.json` for saved registry settings - - Uses provided parameters to override config values - - Defaults to Docker Hub if no registry specified - -2. **Check Docker Installation** - - Verifies Docker is installed and running - - Ensures Docker daemon is accessible - -3. **Build Image (if requested)** - - Runs `docker build` or `docker compose build` - - Creates image with project name - -4. **Check Image Existence** - - Verifies local image exists - - Prompts to build if image not found - -5. **Determine Final Image Name** - - Constructs registry-specific image path - - Applies namespace and tag - - Format varies by registry type - -6. **Tag Image** - - Tags local image with registry path - - Example: `myapp:latest` → `username/myapp:v1.0.0` - -7. **Authenticate (if needed)** - - Uses provided password to login - - Otherwise uses existing Docker credentials - -8. **Push Image** - - Uploads image layers to registry - - Shows progress and completion status - -### Remote Push Process - -1. **Load Server Configuration** - - Reads `deploy-servers.txt` or `deploy-servers.json` - - Filters servers if `--servers` specified - -2. **For Each Server:** - - Test SSH connection - - Verify image exists on remote server - - Tag image with registry path (if custom tag specified) - - Login to registry on remote server - - Execute `docker push` command - - Report success/failure - -3. **Summary** - - Display push results for all servers - - Show success and failure counts - -### Image Naming Convention - -The command automatically constructs the correct image name based on registry type: - -| Registry | Image Format | Example | -|----------|-------------|---------| -| Docker Hub | `username/projectname:tag` | `myuser/myapp:v1.0.0` | -| ECR | `account.dkr.ecr.region.amazonaws.com/repo:tag` | `123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:v1.0.0` | -| GCR | `gcr.io/project-id/repo:tag` | `gcr.io/my-project/myapp:v1.0.0` | -| ACR | `registry.azurecr.io/repo:tag` | `myregistry.azurecr.io/myapp:v1.0.0` | -| GHCR | `ghcr.io/username/repo:tag` | `ghcr.io/myuser/myapp:v1.0.0` | -| Private | `registry.domain.com/repo:tag` | `registry.company.com/myapp:v1.0.0` | - -## Complete Workflow Examples - -### Workflow 1: First-Time Push to Docker Hub - -```bash -# Step 1: Initialize Docker files -wheels docker init - -# Step 2: Login to Docker Hub -wheels docker login --registry=dockerhub --username=myuser - -# Step 3: Build and push -wheels docker push --local --build -``` - -### Workflow 2: Version Release to Docker Hub - -```bash -# Build image -wheels docker build --local --tag=myapp:v1.0.0 - -# Push with version tag -wheels docker push --local --tag=v1.0.0 - -# Also push as latest -wheels docker push --local --tag=latest -``` - -### Workflow 3: Push to Multiple Registries - -```bash -# Push to Docker Hub -wheels docker login --registry=dockerhub --username=myuser -wheels docker push --local --registry=dockerhub --username=myuser --tag=v1.0.0 - -# Push to GitHub Container Registry -wheels docker login --registry=ghcr --username=myuser --password=ghp_token -wheels docker push --local --registry=ghcr --username=myuser --tag=v1.0.0 - -# Push to ECR -wheels docker login --registry=ecr --image=123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp -wheels docker push --local --registry=ecr --image=123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:v1.0.0 -``` - -### Workflow 4: Development → Staging → Production - -```bash -# Development (local testing) -wheels docker build --local -wheels docker push --local --tag=dev - -# Staging (build and push to staging) -wheels docker build --local --tag=myapp:staging --nocache -wheels docker push --local --tag=staging - -# Production (clean build and push) -wheels docker build --local --tag=myapp:v1.0.0 --nocache --pull -wheels docker push --local --tag=v1.0.0 -wheels docker push --local --tag=latest -``` - -### Workflow 5: Remote Server Push - -```bash -# Build on remote servers -wheels docker build --remote - -# Push from remote servers to registry -wheels docker push --remote --registry=dockerhub --username=myuser --password=mytoken - -# Or push from specific servers only -wheels docker push --remote --servers=1,3 --registry=dockerhub --username=myuser -``` - -## Docker Requirements - -### Local Requirements - -- Docker Desktop (Mac/Windows) or Docker Engine (Linux) installed and running -- Docker daemon accessible -- Image already built (or use `--build` flag) -- Authentication credentials (via `wheels docker login` or `--password`) - -### Remote Requirements - -- SSH access to remote servers -- Docker installed on remote servers -- Built image on remote server -- Server configuration file (`deploy-servers.txt` or `deploy-servers.json`) - -## Troubleshooting - -### Common Issues - -#### Authentication Failed - -**Error:** "unauthorized: authentication required" or "denied: requested access to the resource is denied" - -**Solutions:** - -```bash -# Login first -wheels docker login --registry=dockerhub --username=myuser - -# Or provide password explicitly -wheels docker push --local --password=mytoken - -# For Docker Hub, use access token instead of password -wheels docker login --registry=dockerhub --username=myuser --password=dckr_pat_abc123 -``` - -#### Image Not Found - -**Error:** "Local image 'projectname:latest' not found" - -**Solutions:** - -```bash -# Build the image first -wheels docker build --local - -# Or use --build flag to build automatically -wheels docker push --local --build - -# Check if image exists -docker images -``` - -#### Tag Already Exists - -**Error:** "tag already exists" or attempting to overwrite existing tag - -**Solution:** -```bash -# Use a new version tag -wheels docker push --local --tag=v1.0.1 - -# Or force overwrite by building and pushing again -wheels docker push --local --build --tag=v1.0.0 -``` - -#### Wrong Image Name Format - -**Error:** "invalid reference format" or "repository name must be lowercase" - -**Solutions:** - -```bash -# Ensure correct format for registry -# Docker Hub: username/repo:tag -wheels docker push --local --registry=dockerhub --username=myuser --tag=v1.0.0 - -# ECR: Include full registry URL -wheels docker push --local --registry=ecr --image=123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:v1.0.0 - -# Check current images -docker images -``` - -#### ECR Authentication Expiry - -**Error:** "no basic auth credentials" or "token expired" for ECR - -**Solution:** - -ECR tokens expire after 12 hours. Re-authenticate: - -```bash -# Re-login to ECR -wheels docker login --registry=ecr --image=123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp - -# Then push again -wheels docker push --local --registry=ecr --image=123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest -``` - -#### Network Timeout - -**Error:** "net/http: TLS handshake timeout" or push taking too long - -**Solutions:** - -```bash -# Check internet connection -ping hub.docker.com - -# Check Docker daemon -docker ps - -# Try pushing again -wheels docker push --local - -# For large images, push may take time - be patient -``` - -#### Permission Denied (Remote) - -**Error:** "permission denied" when pushing from remote server - -**Solutions:** - -```bash -# SSH into remote server and check -ssh user@host - -# Ensure user can run docker -docker ps - -# Add user to docker group if needed -sudo usermod -aG docker $USER -newgrp docker - -# Or use sudo (not recommended for production) -``` - -#### Registry Not Supported - -**Error:** "Unsupported registry: xyz" - -**Solution:** - -```bash -# Check supported registries -# dockerhub, ecr, gcr, acr, ghcr, private - -# Use correct registry name -wheels docker push --local --registry=dockerhub --username=myuser -``` - -### Debugging Push Issues - -```bash -# Check Docker credentials -cat ~/.docker/config.json - -# Verify you can login manually -docker login - -# Check if image exists locally -docker images - -# Try manual push to test -docker tag myapp:latest username/myapp:latest -docker push username/myapp:latest - -# Check docker-config.json -cat docker-config.json -``` - -## Advanced Usage - -### Multi-Architecture Images - -Push images for multiple architectures: - -```bash -# Build for multiple platforms (requires Docker Buildx) -docker buildx create --use -docker buildx build --platform linux/amd64,linux/arm64 -t username/myapp:v1.0.0 --push . - -# Or push separately -wheels docker build --local --tag=myapp:amd64 -wheels docker push --local --tag=v1.0.0-amd64 - -wheels docker build --local --tag=myapp:arm64 -wheels docker push --local --tag=v1.0.0-arm64 -``` - -### Automated Tagging Strategy - -```bash -#!/bin/bash -# auto-tag-push.sh - -# Get version from VERSION file -VERSION=$(cat VERSION) -COMMIT=$(git rev-parse --short HEAD) -DATE=$(date +%Y%m%d) - -# Build once -wheels docker build --local - -# Push multiple tags -wheels docker push --local --tag=$VERSION -wheels docker push --local --tag=$VERSION-$COMMIT -wheels docker push --local --tag=$VERSION-$DATE -wheels docker push --local --tag=latest -``` - -### Conditional Pushing - -```bash -#!/bin/bash -# conditional-push.sh - -# Only push on main branch -if [ "$GIT_BRANCH" == "main" ]; then - echo "Main branch detected, pushing to production registry" - wheels docker push --local --registry=dockerhub --username=produser --tag=latest -elif [ "$GIT_BRANCH" == "develop" ]; then - echo "Develop branch detected, pushing to staging registry" - wheels docker push --local --registry=dockerhub --username=staginguser --tag=develop -else - echo "Feature branch, skipping push" -fi -``` - -### Image Size Optimization Before Push - -```bash -# Build with optimization -wheels docker build --local --nocache - -# Check image size -docker images myapp:latest - -# If too large, optimize Dockerfile and rebuild -# Then push -wheels docker push --local -``` - -## Best Practices - -### 1. Always Tag Production Images - -```bash -# Bad - using only latest -wheels docker push --local --tag=latest - -# Good - use semantic versioning -wheels docker push --local --tag=v1.0.0 -wheels docker push --local --tag=latest -``` - -### 2. Use Access Tokens, Not Passwords - -```bash -# Bad - using account password -wheels docker push --local --password=mypassword123 - -# Good - using access token -wheels docker push --local --password=dckr_pat_abc123xyz -``` - -### 3. Separate Development and Production Images - -```bash -# Development -wheels docker push --local --tag=dev - -# Staging -wheels docker push --local --tag=staging - -# Production -wheels docker push --local --tag=v1.0.0 -wheels docker push --local --tag=latest -``` - -### 4. Test Images Locally Before Pushing - -```bash -# Build and test -wheels docker build --local -docker run -d -p 8080:8080 myapp:latest - -# Test the application -curl http://localhost:8080 - -# If successful, push -wheels docker push --local -``` - -### 5. Use Meaningful Tags - -```bash -# Good tagging examples -wheels docker push --local --tag=v1.0.0 # Semantic version -wheels docker push --local --tag=v1.0.0-hotfix1 # Hotfix version -wheels docker push --local --tag=20241224 # Date-based -wheels docker push --local --tag=prod-stable # Environment + status -``` - -### 6. Keep Build and Push Separate for Large Images - -```bash -# Build separately (can take time) -wheels docker build --local --nocache - -# Push when ready -wheels docker push --local --tag=v1.0.0 -``` - -### 7. Clean Up Old Images Locally - -```bash -# After pushing, clean up local images to save space -docker images -docker rmi old-image:old-tag - -# Or prune unused images -docker image prune -a -``` - -### 8. Document Your Registry Structure - -Create a `REGISTRY.md` in your project: - -```markdown -# Docker Registry Structure - -## Docker Hub (Production) -- `username/myapp:latest` - Latest stable release -- `username/myapp:v*` - Versioned releases - -## GHCR (Development) -- `ghcr.io/username/myapp:dev` - Development builds -- `ghcr.io/username/myapp:pr-*` - PR builds - -## ECR (Internal) -- `123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:staging` - Staging -``` - -### 9. Automate Version Bumping - +**Push from Specific Servers** +Only trigger the push on specific servers. ```bash -#!/bin/bash -# bump-and-push.sh - -# Increment version -npm version patch # or use semantic-release - -# Get new version -VERSION=$(node -p "require('./package.json').version") - -# Build and push -wheels docker build --local -wheels docker push --local --tag=v$VERSION -wheels docker push --local --tag=latest - -# Git tag -git tag v$VERSION -git push --tags +wheels docker push --remote --servers=1 --registry=dockerhub --username=myuser ``` -### 10. Monitor Push Metrics - -Track push times and sizes for optimization: - -```bash -# Before push -START_TIME=$(date +%s) - -# Push -wheels docker push --local --tag=v1.0.0 - -# After push -END_TIME=$(date +%s) -DURATION=$((END_TIME - START_TIME)) -echo "Push took $DURATION seconds" -``` - -## Security Considerations - -### 1. Never Commit Credentials - -```bash -# Add to .gitignore -echo "docker-config.json" >> .gitignore -echo ".dockercfg" >> .gitignore -echo "*.pem" >> .gitignore -``` - -### 2. Use Registry Access Controls - -- Enable private repositories for sensitive images -- Use registry webhooks for security scanning -- Implement image signing for production - -### 3. Scan Images Before Pushing - -```bash -# Install trivy or similar scanner -docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ - aquasec/trivy image myapp:latest - -# Only push if scan passes -wheels docker push --local --tag=v1.0.0 -``` - -### 4. Limit Registry Permissions - -- Use read-only tokens for pulling -- Use write tokens for push operations -- Rotate tokens regularly - -### 5. Enable Image Vulnerability Scanning - -Most registries offer built-in scanning: -- Docker Hub: Enable Vulnerability Scanning -- ECR: Enable Enhanced Scanning -- GHCR: Enable Dependabot -- ACR: Enable Microsoft Defender - -## Related Commands - -- [wheels docker init](docker-init.md) - Initialize Docker configuration files -- [wheels docker build](docker-build.md) - Build Docker images -- [wheels docker login](docker-login.md) - Authenticate with registries -- [wheels docker deploy](docker-deploy.md) - Build and deploy Docker containers -- [wheels docker logs](docker-logs.md) - View container logs -- [wheels docker exec](docker-exec.md) - Execute commands in containers -- [wheels docker stop](docker-stop.md) - Stop Docker containers - ---- +## Configuration + + This command prioritizes settings in this order: + 1. Command-line arguments (`--image`, `--tag`, etc.) + 2. Centralized configuration in `config/deploy.yml` + 3. Saved session data in `docker-config.json` (created by `wheels docker login`) + +## Workflow Explained -**Note**: This command is part of the Wheels CLI tool suite for Docker management. \ No newline at end of file +1. **Tagging**: The command intelligently determines the target image name: + * **Custom Name**: If your image name contains a colon (e.g., `myregistry:port/path`), it is used directly as the target. + * **Suffix Tagging**: If no colon is present, it appends the project name and tag to the registry/namespace (e.g., `registry/namespace/project:tag`). +2. **Authentication**: + * Checks for interactive password if not provided. + * Assumes existing session if previously logged in via `wheels docker login`. + * On remote servers, it uses the credentials provided in the command to authenticate before pushing. +3. **Pushing**: It executes the `docker push` command for the resolved image. diff --git a/docs/src/command-line-tools/commands/docker/docker-stop.md b/docs/src/command-line-tools/commands/docker/docker-stop.md index 667fdb4f0..768d41c9f 100644 --- a/docs/src/command-line-tools/commands/docker/docker-stop.md +++ b/docs/src/command-line-tools/commands/docker/docker-stop.md @@ -1,768 +1,71 @@ -# Wheels Docker Stop Command Guide +# wheels docker stop -## Overview +Unified Docker stop command for Wheels apps. Stops containers locally or on remote servers. -The `wheels docker stop` command provides a unified interface to stop Docker containers for your Wheels application on both local machines and remote servers. It intelligently detects whether you're using Docker Compose or standard Docker commands and handles container shutdown accordingly. - ---- - -## Command Syntax +## Synopsis ```bash -wheels docker stop [mode] [options] +wheels docker stop [options] ``` -### Modes - -- `--local` - Stop containers on local machine (default if no mode specified) -- `--remote` - Stop containers on remote server(s) - ---- - -## Parameters Reference +## Description -| Parameter | Type | Required | Default | Description | -|-----------|------|----------|---------|-------------| -| `local` | Boolean | No | true* | Stop containers on local machine | -| `remote` | Boolean | No | false | Stop containers on remote server(s) | -| `servers` | String | No | "" | Comma-separated list of server numbers to stop (e.g., "1,3,5") - for remote only | -| `removeContainer` | Boolean | No | false | Also remove the container after stopping | +The `wheels docker stop` command halts running containers. It supports stopping containers locally or across multiple remote servers. -**Note:** If neither `--local` nor `--remote` is specified, `--local` is used by default. +**Centralized Configuration**: +- **Source of Truth**: This command prioritizes settings from `config/deploy.yml` for server lists and project names, ensuring you only stop the containers relevant to your current project. ---- +## Options -## Usage Examples +| Option | Description | Default | +|--------|-------------|---------| +| `--local` | Stop containers on local machine | `false` | +| `--remote` | Stop containers on remote server(s) | `false` | +| `--servers` | Specific servers to stop (defaults to `config/deploy.yml`) | `""` | +| `--removeContainer` | Also remove the container after stopping | `false` | -### Local Operations - -#### Basic Local Stop (Default) - -Stop containers on your local machine: - -```bash -wheels docker stop -``` +## Detailed Examples -or explicitly: +### Local Management +**Stop Services** +Stops the running containers but keeps them (and their logs/state) intact. ```bash wheels docker stop --local ``` -#### Stop and Remove Local Container - -Stop the container and also remove it: - +**Stop and Cleanup** +Stops the containers and removes them. This is useful for a full reset or to free up names. ```bash wheels docker stop --local --removeContainer ``` -#### Stop Local Docker Compose Services - -If you have a `docker-compose.yml` or `docker-compose.yaml` file, it will automatically use Docker Compose: - -```bash -wheels docker stop --local -``` - -**Output:** -``` -Found docker-compose file, will stop docker-compose services -Stopping services with docker-compose... -Docker Compose services stopped successfully! -``` - ---- - -### Remote Operations - -#### Stop All Remote Servers - -Stop containers on all servers defined in your configuration file: +### Remote Management +**Stop All Remote Servers** +Halts the application on all configured servers. Useful for maintenance windows. ```bash wheels docker stop --remote ``` -#### Stop Specific Remote Servers - -Stop containers only on specific servers (by their position in the config file): - +**Stop Specific Servers** +Stops the application only on specific servers (e.g., taking a node out of rotation). ```bash -wheels docker stop --remote --servers=1,3,5 +wheels docker stop --remote --servers=1 ``` -This stops containers on the 1st, 3rd, and 5th servers in your configuration. - -#### Stop and Remove on Remote Servers - -Stop and remove containers on all remote servers: - +**Full Remote Cleanup** +Stops and removes containers on remote servers. ```bash wheels docker stop --remote --removeContainer ``` -#### Stop Specific Servers and Remove - -Combine server selection with container removal: - -```bash -wheels docker stop --remote --servers=2,4 --removeContainer -``` - ---- - -## Advanced Usage Examples - -### Development Workflow - -#### Clean Restart Local Environment - -Stop and remove local containers for a clean restart: - -```bash -wheels docker stop --local --removeContainer -``` - -Then start fresh: -```bash -wheels docker deploy local -``` - -#### Stop Staging Servers - -If servers 1-3 are staging in your config: - -```bash -wheels docker stop --remote --servers=1,2,3 -``` - -#### Stop Production Servers - -If servers 4-6 are production: - -```bash -wheels docker stop --remote --servers=4,5,6 -``` - -### Maintenance Scenarios - -#### Rolling Restart - Stop Half - -Stop half of your servers for maintenance: - -```bash -# Stop servers 1, 3, 5 (odd-numbered) -wheels docker stop --remote --servers=1,3,5 -``` - -Perform maintenance, then restart: -```bash -wheels docker deploy remote --servers=deploy-servers.json -``` - -Then stop the other half: -```bash -# Stop servers 2, 4, 6 (even-numbered) -wheels docker stop --remote --servers=2,4,6 -``` - -#### Emergency Stop All - -Quickly stop all remote containers: - -```bash -wheels docker stop --remote -``` - -#### Complete Cleanup - -Stop and remove all containers on all servers: - -```bash -wheels docker stop --remote --removeContainer -``` - ---- - -## Server Configuration - -The `wheels docker stop` command uses the same server configuration files as other Docker commands. - -### Configuration File Locations - -The command automatically looks for these files in your project root: - -1. `deploy-servers.txt` (simple text format) -2. `deploy-servers.json` (detailed JSON format) - -### deploy-servers.txt Format - -Simple format with space or tab-separated values: - -```text -192.168.1.100 ubuntu 22 -production.example.com deploy 22 -staging.example.com deploy 2222 -``` - -**Format:** `host user [port]` -- `host` - Server hostname or IP address (required) -- `user` - SSH username (required) -- `port` - SSH port (optional, default: 22) - -**Example with comments:** -```text -## Production Servers -web1.example.com deploy 22 -web2.example.com deploy 22 - -## Staging Servers -staging1.example.com deploy 22 -staging2.example.com deploy 22 -``` - -### deploy-servers.json Format - -Detailed format with full configuration options: - -```json -{ - "servers": [ - { - "host": "web1.example.com", - "user": "deploy", - "port": 22, - "imageName": "myapp", - "remoteDir": "/home/deploy/myapp" - }, - { - "host": "web2.example.com", - "user": "deploy", - "port": 2222, - "imageName": "myapp", - "remoteDir": "/opt/applications/myapp" - }, - { - "host": "staging.example.com", - "user": "ubuntu", - "port": 22, - "imageName": "myapp-staging", - "remoteDir": "/home/ubuntu/myapp-staging" - } - ] -} -``` - -**JSON Parameters:** -- `host` - Server hostname or IP address (required) -- `user` - SSH username (required) -- `port` - SSH port (optional, default: 22) -- `imageName` - Docker container/image name (optional, default: project name) -- `remoteDir` - Remote application directory (optional, default: `/home/[user]/[project-name]`) - ---- - -## Server Selection - -### Selecting Specific Servers - -When using the `--servers` parameter, you specify servers by their position (1-indexed) in the configuration file: - -**Example Configuration:** -```text -# Server 1 -web1.example.com deploy 22 -# Server 2 -web2.example.com deploy 22 -# Server 3 -web3.example.com deploy 22 -# Server 4 -db.example.com deploy 22 -``` - -**Stop only servers 1 and 3:** -```bash -wheels docker stop --remote --servers=1,3 -``` - -**Stop servers 2, 3, and 4:** -```bash -wheels docker stop --remote --servers=2,3,4 -``` - -### Invalid Server Numbers - -If you specify invalid server numbers, they will be skipped: - -```bash -wheels docker stop --remote --servers=1,99,3 -``` - -**Output:** -``` -Skipping invalid server number: 99 -Selected 2 of 4 server(s) -``` - -If no valid servers are selected, all servers will be used: -```bash -wheels docker stop --remote --servers=99,100 -``` - -**Output:** -``` -Skipping invalid server number: 99 -Skipping invalid server number: 100 -No valid servers selected, using all servers -``` - ---- - -## Docker Compose vs Standard Docker - -The stop command intelligently detects which approach to use: - -### Docker Compose Mode - -**Detection:** Looks for `docker-compose.yml` or `docker-compose.yaml` in: -- Local: Your current project directory -- Remote: The `remoteDir` on the server - -**Command Used:** -```bash -docker compose down -``` - -**Benefits:** -- Stops all services defined in the compose file -- Removes networks created by compose -- Cleaner multi-container shutdown - -### Standard Docker Mode - -**Used When:** No Docker Compose file is found - -**Command Used:** -```bash -docker stop [container-name] -``` - -**Optional Container Removal:** -```bash -docker rm [container-name] -``` - ---- - -## Container Naming - -The command uses your project name to identify containers: - -### Project Name Derivation - -Project name is derived from your current directory: -- Converted to lowercase -- Non-alphanumeric characters replaced with hyphens -- Multiple consecutive hyphens reduced to single hyphen -- Leading/trailing hyphens removed - -**Examples:** -- Directory: `MyWheelsApp` → Container: `mywheelsapp` -- Directory: `wheels-blog-v2` → Container: `wheels-blog-v2` -- Directory: `My App 2024` → Container: `my-app-2024` - -### Custom Container Names - -You can specify custom container names in `deploy-servers.json`: - -```json -{ - "servers": [ - { - "host": "web1.example.com", - "user": "deploy", - "imageName": "custom-app-name" - } - ] -} -``` - ---- - -## Sudo Handling on Remote Servers - -The command intelligently determines whether to use `sudo` for Docker commands: - -### Check Performed - -```bash -if groups | grep -q docker && [ -w /var/run/docker.sock ]; then - docker stop [container] -else - sudo docker stop [container] -fi -``` - -### Requirements for No-Sudo Operation - -1. User is in the `docker` group -2. User has write access to `/var/run/docker.sock` - -### Adding User to Docker Group - -On the remote server: -```bash -sudo usermod -aG docker deploy -# Log out and back in for changes to take effect -``` - ---- - -## Error Handling and Recovery - -### Container Not Running - -If a container is not running, the command will display a warning but not fail: - -```bash -wheels docker stop --local -``` - -**Output:** -``` -Stopping Docker container 'myapp'... -Container might not be running: No such container -``` - -This is normal behavior and allows the command to complete successfully. - -### SSH Connection Failures - -**Error:** "SSH connection failed to web1.example.com. Check credentials and access." - -**Solutions:** -1. Verify SSH connectivity: - ```bash - ssh user@host - ``` - -2. Check SSH key authentication: - ```bash - ssh-add -l - ``` - -3. Test with verbose output: - ```bash - ssh -v user@host - ``` - -4. Verify port in configuration (default: 22) - -### No Configuration File - -**Error:** "No server configuration found. Create deploy-servers.txt or deploy-servers.json in your project root." - -**Solution:** Create one of the configuration files: - -**Quick setup (deploy-servers.txt):** -```bash -echo "your-server.com deploy 22" > deploy-servers.txt -``` - -**Or JSON format:** -```bash -cat > deploy-servers.json << 'EOF' -{ - "servers": [ - { - "host": "your-server.com", - "user": "deploy", - "port": 22 - } - ] -} -EOF -``` - -### Docker Not Installed Locally - -**Error:** "Docker is not installed or not accessible. Please ensure Docker Desktop or Docker Engine is running." - -**Solutions:** -1. Install Docker Desktop (macOS/Windows) -2. Install Docker Engine (Linux) -3. Start Docker service: - ```bash - # macOS/Windows: Start Docker Desktop - # Linux: - sudo systemctl start docker - ``` - -4. Verify installation: - ```bash - docker --version - ``` - ---- - -## Operation Summary - -When stopping multiple servers, a summary is displayed: - -```bash -wheels docker stop --remote --servers=1,2,3 -``` - -**Output:** -``` -Stopping containers on 3 server(s)... - ---------------------------------------- -Stopping container on server 1 of 3: web1.example.com ---------------------------------------- -... -Container on web1.example.com stopped successfully - ---------------------------------------- -Stopping container on server 2 of 3: web2.example.com ---------------------------------------- -... -Container on web2.example.com stopped successfully - ---------------------------------------- -Stopping container on server 3 of 3: web3.example.com ---------------------------------------- -... -Container on web3.example.com stopped successfully - -Stop Operations Summary: - Successful: 3 -``` - -If any servers fail: -``` -Stop Operations Summary: - Successful: 2 - Failed: 1 -``` - ---- - -## Best Practices - -### 1. Always Test Locally First - -Before stopping remote servers: -```bash -# Test the stop command locally -wheels docker stop --local - -# Verify it works as expected -docker ps -a -``` - -### 2. Use Server Selection for Staged Rollouts - -Don't stop all servers at once in production: -```bash -# Stop first half -wheels docker stop --remote --servers=1,2 - -# Verify, then stop second half -wheels docker stop --remote --servers=3,4 -``` - -### 3. Remove Containers During Maintenance - -For major updates or troubleshooting: -```bash -wheels docker stop --remote --removeContainer -``` - -This ensures a completely clean slate for the next deployment. - -### 4. Check Status After Stopping - -Verify containers are stopped: -```bash -wheels docker stop --local -docker ps -a -``` - -### 5. Keep Configuration Files Secure - -**Security recommendations:** -- Store configuration files outside version control (add to `.gitignore`) -- Use restrictive file permissions: - ```bash - chmod 600 deploy-servers.txt - chmod 600 deploy-servers.json - ``` -- Use SSH keys instead of passwords -- Consider encrypting sensitive configuration - -### 6. Document Server Numbers - -Add comments to your configuration files: -```text -## Production Web Servers (1-3) -web1.example.com deploy 22 -web2.example.com deploy 22 -web3.example.com deploy 22 - -## Production Database (4) -db.example.com deploy 22 - -## Staging (5-6) -staging1.example.com deploy 22 -staging2.example.com deploy 22 -``` - -### 7. Use Descriptive Server Names - -In JSON format, make server purposes clear: -```json -{ - "servers": [ - { - "host": "prod-web-1.example.com", - "user": "deploy", - "imageName": "myapp-production" - }, - { - "host": "staging-web-1.example.com", - "user": "deploy", - "imageName": "myapp-staging" - } - ] -} -``` - ---- - -## Common Workflows - -### Development Cycle - -```bash -# 1. Stop local containers -wheels docker stop --local --removeContainer - -# 2. Make code changes -# ... edit files ... - -# 3. Rebuild and restart -wheels docker build local -wheels docker deploy local -``` - -### Deployment Update - -```bash -# 1. Build new image -wheels docker build remote - -# 2. Stop old containers (staged) -wheels docker stop --remote --servers=1,3,5 - -# 3. Deploy new version -wheels docker deploy remote --servers=1,3,5 - -# 4. Stop remaining servers -wheels docker stop --remote --servers=2,4,6 - -# 5. Deploy to remaining servers -wheels docker deploy remote --servers=2,4,6 -``` - -### Emergency Rollback - -```bash -# 1. Stop all production servers -wheels docker stop --remote --servers=1,2,3,4 - -# 2. Deploy previous version -wheels docker deploy remote --servers=production-servers.json - -# 3. Verify deployment -wheels docker logs --remote --servers=1 tail=100 -``` - -### Complete Cleanup - -```bash -# Stop and remove everything -wheels docker stop --local --removeContainer -wheels docker stop --remote --removeContainer -``` - ---- - -## Integration with Other Commands - -### Check Status Before Stopping - -```bash -# Check which containers are running -wheels docker status - -# Then stop specific ones -wheels docker stop --remote --servers=2,3 -``` - -### View Logs Before Stopping - -```bash -# Check for errors -wheels docker logs tail=100 - -# If issues found, stop and restart -wheels docker stop --local -wheels docker deploy local -``` - -### Complete Stop and Redeploy - -```bash -# Stop with removal -wheels docker stop --remote --removeContainer - -# Rebuild -wheels docker build remote - -# Deploy fresh -wheels docker deploy remote -``` - ---- - -## Related Commands - -- [wheels docker init](docker-init.md) - Initialize Docker configuration files -- [wheels docker build](docker-build.md) - Build Docker images -- [wheels docker deploy](docker-deploy.md) - Build and deploy Docker containers -- [wheels docker push](docker-push.md) - Push Docker images to registries -- [wheels docker login](docker-login.md) - Authenticate with registries -- [wheels docker logs](docker-logs.md) - View container logs -- [wheels docker exec](docker-exec.md) - Execute commands in containers - ---- - -**Note**: This command is part of the Wheels CLI tool suite for Docker management. - ---- - -## Additional Notes +## How It Works -- Default mode is `--local` if neither `--local` nor `--remote` is specified -- Cannot specify both `--local` and `--remote` simultaneously -- SSH connections use key-based authentication (password auth not supported) -- Containers are stopped gracefully with default Docker stop timeout (10 seconds) -- Docker Compose down removes networks automatically -- The `--removeContainer` flag only removes the container, not the image -- Server selection is 1-indexed (starts at 1, not 0) -- Invalid server numbers are skipped with warnings -- The command attempts to detect sudo requirements automatically -- Exit status reflects overall operation success \ No newline at end of file +1. **Environment Detection**: It checks for `docker-compose.yml`. +2. **Graceful Shutdown**: + * **Docker Compose**: Runs `docker compose down`. This stops and removes containers, networks, and volumes defined in the compose file. + * **Standard Docker**: Runs `docker stop [container_name]`. This sends `SIGTERM` to the main process, allowing it to shut down gracefully. +3. **Removal (`--removeContainer`)**: + * If using standard Docker, this flag triggers `docker rm [container_name]` after stopping. + * If using Docker Compose, `down` already removes containers, so this flag is redundant but harmless. From cea010d832fea44adedd5c73e6421dd35d53a0ca Mon Sep 17 00:00:00 2001 From: Zain Ul Abideen Date: Mon, 5 Jan 2026 20:40:15 +0500 Subject: [PATCH 2/2] cli/duplicate function remove reconstructArgs( ) already exists in parent file BaseCommand.cfc --- .../commands/wheels/docker/DockerCommand.cfc | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/cli/src/commands/wheels/docker/DockerCommand.cfc b/cli/src/commands/wheels/docker/DockerCommand.cfc index 657467034..fa4aeef21 100644 --- a/cli/src/commands/wheels/docker/DockerCommand.cfc +++ b/cli/src/commands/wheels/docker/DockerCommand.cfc @@ -609,26 +609,6 @@ component extends="../base" { return ""; } - /** - * Reconstruct arguments to handle key=value pairs passed as keys - */ - public function reconstructArgs(required struct args) { - var newArgs = duplicate(arguments.args); - - // Check for args in format "key=value" which CommandBox sometimes passes as keys with empty values - for (var key in newArgs) { - if (find("=", key)) { - var parts = listToArray(key, "="); - if (arrayLen(parts) >= 2) { - var paramName = parts[1]; - var paramValue = right(key, len(key) - len(paramName) - 1); - newArgs[paramName] = paramValue; - } - } - } - - return newArgs; - } // ============================================================================= // SHARED HELPER FUNCTIONS (Moved from deploy.cfc)