基于 Node.js 和 SSH2 的 Docker 自动化部署实践

基于 Node.js 和 SSH2 的 Docker 自动化部署实践

前言

在现代 Web 应用开发中,自动化部署是提高开发效率、减少人为错误的关键环节。本文将分享一套基于 Node.js 开发的 Docker 自动化部署方案,该方案通过 SSH 连接远程服务器,实现了智能版本检测、自动容器管理和镜像更新等功能。

技术架构

核心技术栈

  • Node.js: 脚本运行环境
  • ssh2: SSH 客户端库,用于远程服务器连接和命令执行
  • Docker: 容器化部署平台
  • Docker Registry: 镜像仓库管理

系统架构图

markdown 复制代码
┌─────────────┐         SSH          ┌──────────────┐
│  本地开发机  │ ─────────────────────→│  远程服务器   │
│             │                       │              │
│ Node.js     │                       │  Docker      │
│ 部署脚本     │                       │  容器运行时   │
└─────────────┘                       └──────────────┘
       │                                      ↑
       │                                      │
       ↓                                      │
┌─────────────┐         Pull Image           │
│ Docker      │ ─────────────────────────────┘
│ Registry    │
└─────────────┘

核心功能设计

1. 智能版本检测机制

版本检测是自动化部署的核心功能之一。系统采用三层检测策略,确保能够准确获取远程仓库的最新版本。

方法一:Skopeo 工具(推荐)

Skopeo 是一个专门用于操作容器镜像的命令行工具,可以直接从仓库获取完整的标签列表。

bash 复制代码
skopeo list-tags docker://registry.example.com/namespace/image-name \
  --creds username:password | jq -r '.Tags[]' | \
  grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1

优点

  • 最准确,能获取所有标签
  • 速度快,不需要预先拉取镜像
  • 支持多种镜像仓库

要求

  • 服务器上需要安装 skopeojq 工具
方法二:Registry API

直接调用 Docker Registry HTTP API V2 获取标签列表。

bash 复制代码
curl -s -u "username:password" \
  "https://registry.example.com/v2/namespace/image-name/tags/list" \
  | jq -r '.tags[]' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | \
  sort -V | tail -1

优点

  • 直接调用 API,获取所有标签
  • 标准化接口,兼容性好

要求

  • 服务器上需要安装 jq 工具
  • 需要正确的 API 认证
方法三:Manifest 检查(备用)

使用 docker manifest inspect 命令逐个检查可能存在的版本。

javascript 复制代码
// 从当前版本开始,向上检查可能存在的版本
const [major, minor, patch] = baseVersion.split('.').map(Number)

// 生成要检查的版本列表(向上检查 10 个 patch 版本)
const versionsToCheck = []
for (let i = 1; i <= 10; i++) {
  versionsToCheck.push(`${major}.${minor}.${patch + i}`)
}

// 逐个检查版本是否存在
for (const version of versionsToCheck) {
  try {
    await execSSHCommand(
      conn,
      `docker manifest inspect ${fullImageName}:${version} > /dev/null 2>&1`
    )
    latestFound = version
  }
  catch {
    // 版本不存在,停止检查
    break
  }
}

优点

  • 不需要额外工具
  • 使用标准 Docker 命令

缺点

  • 需要逐个尝试,效率较低
  • 检查范围有限(最多 10 个 patch 版本)
  • 可能遗漏跨 minor/major 版本的更新

2. SSH 连接管理

使用 ssh2 库实现可靠的 SSH 连接和命令执行。

javascript 复制代码
function connectSSH(config) {
  return new Promise((resolve, reject) => {
    const conn = new Client()

    conn.on('ready', () => {
      logger.success(`SSH 连接成功: ${config.server.username}@${config.server.host}`)
      resolve(conn)
    }).on('error', (err) => {
      logger.error(`SSH 连接失败: ${err.message}`)
      reject(err)
    }).connect({
      host: config.server.host,
      port: config.server.port,
      username: config.server.username,
      password: config.server.password,
      readyTimeout: 30000,
    })
  })
}

关键特性

  • 支持密码认证(生产环境建议使用密钥认证)
  • 30 秒连接超时设置
  • 完整的错误处理机制

3. 命令执行封装

封装 SSH 命令执行逻辑,提供统一的接口。

javascript 复制代码
function execSSHCommand(conn, command) {
  return new Promise((resolve, reject) => {
    conn.exec(command, (err, stream) => {
      if (err) {
        reject(err)
        return
      }

      let stdout = ''
      let stderr = ''

      stream.on('close', (code, _signal) => {
        if (code !== 0) {
          reject(new Error(`命令执行失败 (退出码: ${code}): ${stderr || stdout}`))
        }
        else {
          resolve(stdout.trim())
        }
      }).on('data', (data) => {
        stdout += data.toString()
      }).stderr.on('data', (data) => {
        stderr += data.toString()
      })
    })
  })
}

特点

  • Promise 化的异步接口
  • 完整的标准输出和错误输出捕获
  • 退出码检查

完整部署流程

流程图

复制代码
开始
  │
  ├─→ 1. 连接远程服务器(SSH)
  │
  ├─→ 2. 检查 Docker 环境
  │
  ├─→ 3. 获取当前容器信息
  │     ├─ 容器名称
  │     ├─ 镜像版本
  │     ├─ 运行状态
  │     └─ 端口映射
  │
  ├─→ 4. 获取远程仓库最新版本
  │     ├─ 尝试 Skopeo 方法
  │     ├─ 尝试 Registry API
  │     └─ 尝试 Manifest 检查
  │
  ├─→ 5. 版本对比
  │     ├─ 版本相同 → 跳过部署
  │     └─ 版本不同 → 继续部署
  │
  ├─→ 6. 停止并删除旧容器
  │
  ├─→ 7. 拉取新镜像
  │
  ├─→ 8. 启动新容器
  │     ├─ 设置端口映射
  │     ├─ 设置重启策略
  │     └─ 设置平台架构
  │
  ├─→ 9. 验证容器状态
  │
  ├─→ 10. 清理旧镜像
  │
  └─→ 完成

核心代码实现

获取容器信息
javascript 复制代码
async function getCurrentContainerInfo(conn, containerName) {
  try {
    // 检查容器是否存在
    const containerExists = await execSSHCommand(
      conn,
      `docker ps -a --filter "name=^${containerName}$" --format "{{.Names}}"`
    )

    if (!containerExists) {
      return null
    }

    // 获取容器详细信息
    const containerInfo = await execSSHCommand(
      conn,
      `docker inspect ${containerName} --format '{{.Config.Image}}|{{.State.Status}}|{{json .NetworkSettings.Ports}}'`
    )

    const [image, status, portsJson] = containerInfo.split('|')
    const ports = JSON.parse(portsJson)

    // 解析镜像版本
    const version = image.split(':')[1] || 'latest'

    return {
      name: containerName,
      image,
      version,
      status,
      ports
    }
  }
  catch (error) {
    return null
  }
}
拉取镜像
javascript 复制代码
async function pullImage(conn, config, version) {
  const fullImageName = `${config.registry}/${config.namespace}/${config.imageName}:${version}`

  // 登录 Docker 仓库
  await execSSHCommand(
    conn,
    `echo "${config.dockerAuth.password}" | docker login -u "${config.dockerAuth.username}" --password-stdin ${config.registry}`
  )

  // 拉取镜像
  await execSSHCommand(
    conn,
    `docker pull --platform ${config.platform} ${fullImageName}`
  )
}
启动容器
javascript 复制代码
async function startContainer(conn, config, version) {
  const fullImageName = `${config.registry}/${config.namespace}/${config.imageName}:${version}`
  const { name, hostPort, containerPort } = config.container

  const dockerRunCmd = `docker run -d \
    --platform ${config.platform} \
    --name ${name} \
    -p ${hostPort}:${containerPort} \
    --restart unless-stopped \
    ${fullImageName}`

  const containerId = await execSSHCommand(conn, dockerRunCmd)

  // 等待容器启动
  await new Promise(resolve => setTimeout(resolve, 2000))

  // 检查容器状态
  const status = await execSSHCommand(
    conn,
    `docker inspect ${name} --format '{{.State.Status}}'`
  )

  if (status !== 'running') {
    throw new Error(`容器状态异常: ${status}`)
  }
}

命令行接口设计

参数系统

脚本支持丰富的命令行参数,满足不同场景的部署需求。

参数 说明 默认值
--image-name <name> 镜像名称 从配置读取
--version <version> 指定部署版本 自动获取最新版本
--host <host> 服务器地址 从配置读取
--port <port> SSH 端口 22
--username <username> SSH 用户名 root
--password <password> SSH 密码 从配置读取
--container-port <port> 容器内部端口 80
--host-port <port> 宿主机端口 8081
--platform <platform> 平台架构 linux/amd64
--force 强制重新部署 false
--help, -h 显示帮助信息 -

使用示例

bash 复制代码
# 基本使用(使用默认配置)
node deploy/index.js

# 指定版本部署
node deploy/index.js --version 1.0.5

# 强制重新部署(即使版本相同)
node deploy/index.js --force

# 自定义服务器和端口
node deploy/index.js --host 192.168.1.100 --host-port 8082

# 部署到 ARM 架构服务器
node deploy/index.js --platform linux/arm64

# 完整参数示例
node deploy/index.js \
  --image-name my-app \
  --version 2.0.0 \
  --host 192.168.1.100 \
  --username admin \
  --host-port 8080 \
  --platform linux/arm64 \
  --force

NPM Scripts 集成

json 复制代码
{
  "scripts": {
    "deploy": "node deploy/index.js",
    "deploy:force": "node deploy/index.js --force"
  }
}

使用方式:

bash 复制代码
# 自动检测版本并部署
pnpm run deploy

# 强制重新部署
pnpm run deploy:force

日志系统设计

彩色日志输出

实现了一个简洁的日志系统,支持不同级别的日志输出。

javascript 复制代码
const logger = {
  info: msg => console.log(`\x1B[36m[INFO]\x1B[0m ${new Date().toLocaleString()} - ${msg}`),
  success: msg => console.log(`\x1B[32m[SUCCESS]\x1B[0m ${new Date().toLocaleString()} - ${msg}`),
  error: msg => console.error(`\x1B[31m[ERROR]\x1B[0m ${new Date().toLocaleString()} - ${msg}`),
  warn: msg => console.warn(`\x1B[33m[WARN]\x1B[0m ${new Date().toLocaleString()} - ${msg}`),
}

日志输出示例

ini 复制代码
[INFO] 2024-12-03 10:00:00 - ========================================
[INFO] 2024-12-03 10:00:00 - Docker 自动部署脚本
[INFO] 2024-12-03 10:00:00 - ========================================
[SUCCESS] 2024-12-03 10:00:01 - SSH 连接成功: root@xxx.xxx.xxx.xxx
[SUCCESS] 2024-12-03 10:00:02 - Docker 环境检查通过
[INFO] 2024-12-03 10:00:03 - 当前容器信息:
[INFO] 2024-12-03 10:00:03 -   镜像: registry.example.com/namespace/app:1.0.0
[INFO] 2024-12-03 10:00:03 -   版本: 1.0.0
[INFO] 2024-12-03 10:00:03 -   状态: running
[INFO] 2024-12-03 10:00:05 - 获取远程仓库最新版本...
[SUCCESS] 2024-12-03 10:00:07 - 远程仓库最新版本: 1.0.1
[INFO] 2024-12-03 10:00:07 - 检测到新版本,开始部署...
[SUCCESS] 2024-12-03 10:00:20 - 部署完成!
[SUCCESS] 2024-12-03 10:00:20 - 版本: 1.0.1
[SUCCESS] 2024-12-03 10:00:20 - 访问地址: http://xxx.xxx.xxx.xxx:8081

安全性考虑

1. 敏感信息管理

问题:脚本中包含服务器密码、Docker 仓库凭证等敏感信息。

解决方案

方案一:使用配置文件

创建 config.json(不提交到 Git):

json 复制代码
{
  "server": {
    "host": "xxx.xxx.xxx.xxx",
    "username": "admin",
    "password": "your-password"
  },
  "dockerAuth": {
    "username": "your-username",
    "password": "your-password"
  }
}

.gitignore 中添加:

arduino 复制代码
deploy/config.json
方案二:使用环境变量
bash 复制代码
export SSH_PASSWORD="your-password"
export DOCKER_PASSWORD="your-docker-password"

node deploy/index.js --password $SSH_PASSWORD
方案三:使用 SSH 密钥认证(推荐)
javascript 复制代码
// 修改连接配置
conn.connect({
  host: config.server.host,
  port: config.server.port,
  username: config.server.username,
  privateKey: require('node:fs').readFileSync('/path/to/private/key'),
  readyTimeout: 30000,
})

2. 安全最佳实践

  1. 不要将包含密码的配置文件提交到版本控制系统
  2. 生产环境使用 SSH 密钥认证而非密码
  3. 定期更新密码和密钥
  4. 使用最小权限原则
  5. 启用 Docker 仓库的访问控制

错误处理与故障排查

常见问题及解决方案

1. SSH 连接失败

错误信息

arduino 复制代码
[ERROR] SSH 连接失败: connect ETIMEDOUT

解决方案

  • 检查服务器地址和端口是否正确
  • 检查服务器防火墙设置
  • 检查 SSH 服务是否运行:systemctl status sshd
  • 测试网络连接:ping server-ip
2. Docker 命令失败

错误信息

csharp 复制代码
[ERROR] Docker 未安装或未运行

解决方案

  • 在服务器上安装 Docker
  • 启动 Docker 服务:systemctl start docker
  • 检查 Docker 状态:docker --version
3. 镜像拉取失败

错误信息

csharp 复制代码
[ERROR] 拉取镜像失败: unauthorized

解决方案

  • 检查 Docker 仓库用户名和密码
  • 手动登录测试:docker login registry.example.com
  • 检查网络连接
  • 检查镜像名称和标签是否正确
4. 容器启动失败

错误信息

csharp 复制代码
[ERROR] 容器状态异常: exited

解决方案

  • 检查端口是否被占用:netstat -tunlp | grep 8081
  • 查看容器日志:docker logs container-name
  • 检查镜像是否正确
  • 检查容器配置参数
5. 版本检测不准确

错误信息

csharp 复制代码
[WARN] 无法获取最新版本

解决方案

  • 在服务器上安装 skopeojq 工具
  • 手动指定版本:node deploy/index.js --version 1.0.5
  • 使用强制部署:node deploy/index.js --force

服务器环境配置

推荐的服务器环境配置:

bash 复制代码
# 1. 安装必要工具
yum install -y skopeo jq curl  # CentOS/RHEL
apt install -y skopeo jq curl  # Ubuntu/Debian

# 2. 验证 Docker
docker --version
docker info

# 3. 登录 Docker 仓库
docker login registry.example.com

# 4. 测试 skopeo
skopeo list-tags docker://registry.example.com/namespace/image-name

# 5. 检查端口占用
netstat -tunlp | grep 8081

性能优化

1. 镜像清理策略

自动清理旧版本镜像,释放磁盘空间:

javascript 复制代码
async function cleanupOldImages(conn, config, currentVersion) {
  const imagePattern = `${config.registry}/${config.namespace}/${config.imageName}`

  // 获取所有旧版本镜像
  const oldImages = await execSSHCommand(
    conn,
    `docker images ${imagePattern} --format "{{.Repository}}:{{.Tag}}" | grep -v ":${currentVersion}$" || true`
  )

  if (oldImages) {
    const images = oldImages.split('\n').filter(img => img.trim())
    for (const image of images) {
      await execSSHCommand(conn, `docker rmi ${image}`)
    }
  }
}

2. 多平台支持

支持 AMD64 和 ARM64 架构:

bash 复制代码
# 部署到 AMD64 服务器(默认)
node deploy/index.js --platform linux/amd64

# 部署到 ARM64 服务器(如树莓派、Apple Silicon 服务器)
node deploy/index.js --platform linux/arm64

3. 容器重启策略

使用 --restart unless-stopped 策略,确保容器在服务器重启后自动启动:

bash 复制代码
docker run -d \
  --restart unless-stopped \
  --name my-app \
  -p 8081:80 \
  my-image:1.0.0

扩展功能建议

1. 健康检查

添加容器健康检查功能:

javascript 复制代码
async function healthCheck(conn, config) {
  const maxRetries = 5
  const retryInterval = 3000

  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await execSSHCommand(
        conn,
        `curl -f http://localhost:${config.container.hostPort}/health || exit 1`
      )
      logger.success('健康检查通过')
      return true
    }
    catch {
      logger.warn(`健康检查失败,重试 ${i + 1}/${maxRetries}`)
      await new Promise(resolve => setTimeout(resolve, retryInterval))
    }
  }

  throw new Error('健康检查失败')
}

2. 回滚功能

支持快速回滚到上一个版本:

bash 复制代码
# 回滚到指定版本
node deploy/index.js --version 1.0.0 --force

3. 多服务器批量部署

支持同时部署到多个服务器:

javascript 复制代码
const servers = [
  { host: '192.168.1.100', port: 8081 },
  { host: '192.168.1.101', port: 8081 },
  { host: '192.168.1.102', port: 8081 }
]

for (const server of servers) {
  await deploy({ ...config, server })
}

4. Webhook 通知

部署完成后发送通知:

javascript 复制代码
async function sendNotification(status, version) {
  const message = status === 'success'
    ? `✅ 部署成功!版本: ${version}`
    : `❌ 部署失败!`

  // 发送到钉钉、企业微信等
  await fetch('webhook-url', {
    method: 'POST',
    body: JSON.stringify({ message })
  })
}

5. 部署日志保存

将部署日志保存到文件:

javascript 复制代码
const fs = require('node:fs')

const logFile = `deploy-${new Date().toISOString()}.log`

const logger = {
  info: (msg) => {
    const log = `[INFO] ${new Date().toLocaleString()} - ${msg}`
    console.log(log)
    fs.appendFileSync(logFile, `${log}\n`)
  },
  // ... 其他日志方法
}

CI/CD 集成

GitHub Actions 示例

yaml 复制代码
name: Deploy to Production

on:
  push:
    tags:
      - 'v*'

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Install dependencies
        run: |
          cd cmeph-front-h5
          pnpm install

      - name: Deploy to server
        env:
          SSH_PASSWORD: ${{ secrets.SSH_PASSWORD }}
          DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
        run: |
          cd cmeph-front-h5
          node deploy/index.js \
            --password $SSH_PASSWORD \
            --version ${GITHUB_REF#refs/tags/v}

GitLab CI 示例

yaml 复制代码
deploy:
  stage: deploy
  only:
    - tags
  script:
    - cd cmeph-front-h5
    - pnpm install
    - node deploy/index.js --version $CI_COMMIT_TAG
  environment:
    name: production

总结

本文介绍了一套完整的 Docker 自动化部署方案,主要特点包括:

  1. 智能版本检测:三层检测策略,确保准确获取最新版本
  2. 自动化流程:从版本检测到容器启动的全自动化
  3. 灵活配置:丰富的命令行参数,支持多种部署场景
  4. 安全可靠:完整的错误处理和安全机制
  5. 易于扩展:模块化设计,便于添加新功能

适用场景

  • 前端应用的自动化部署
  • 微服务的容器化部署
  • 多环境(开发、测试、生产)的部署管理
  • CI/CD 流程集成

未来改进方向

  • 支持 Docker Compose 多容器部署
  • 支持蓝绿部署和金丝雀发布
  • 添加部署前后的钩子脚本
  • 支持配置文件热重载
  • 添加部署统计和监控
  • 支持 Kubernetes 部署

参考资源


作者注:本文基于实际项目经验总结,所有敏感信息已脱敏处理。如有问题或建议,欢迎交流讨论。

相关推荐
溪饱鱼35 分钟前
NextJs + Cloudflare Worker 是出海最佳实践
前端·后端
明川40 分钟前
Android Gradle 学习 - Kts Gradle学习
前端·gradle
祈澈菇凉1 小时前
Next.js 零基础开发博客后台管理系统教程(八):提升用户体验 - 表单状态、加载与基础验证
前端·javascript·ux
电商API大数据接口开发Cris1 小时前
淘宝 API 关键词搜索接口深度解析:请求参数、签名机制与性能优化
前端·数据挖掘·api
小周同学1 小时前
vue3 上传文件,图片,视频组件
前端·vue.js
细心细心再细心1 小时前
runtime-dom记录备忘
前端
小猪努力学前端1 小时前
基于PixiJS的小游戏广告开发
前端·webgl·游戏开发
哆啦A梦15881 小时前
62 对接支付宝沙箱
前端·javascript·vue.js·node.js