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

Junki
Junki
Published on 2025-12-03 / 6 Visits
0
0

在现代 Web 应用开发中,自动化部署是提升效率、减少人为失误的关键。本文分享一套基于 Node.js 开发的 Docker 自动化部署方案,通过 SSH 连接远程服务器,实现智能版本检测、容器生命周期管理与镜像自动更新。

技术架构

核心技术栈

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

系统架构图

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

核心功能设计

1. 智能版本检测(三层策略)

方法一:Skopeo 工具(推荐)

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

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
  • 优势:标准化接口、兼容性好
  • 依赖:服务器安装 jq、正确 API 认证

方法三:Manifest 检查(备用)

const [major, minor, patch] = baseVersion.split('.').map(Number)
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 命令
  • 局限:效率低、检查范围有限

2. SSH 连接与命令执行

SSH 连接封装

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
    })
  })
}

命令执行封装

function execSSHCommand(conn, command) {
  return new Promise((resolve, reject) => {
    conn.exec(command, (err, stream) => {
      if (err) return reject(err)
      let stdout = '', stderr = ''
      stream.on('close', (code) => {
        code !== 0 
          ? reject(new Error(`命令执行失败 (退出码: ${code}): ${stderr || stdout}`))
          : resolve(stdout.trim())
      }).on('data', (data) => stdout += data.toString())
        .stderr.on('data', (data) => stderr += data.toString())
    })
  })
}

完整部署流程

开始 → 1. SSH 连接远程服务器 → 2. 检查 Docker 环境 → 3. 获取当前容器信息
→ 4. 三层策略获取仓库最新版本 → 5. 版本对比(相同则跳过)
→ 6. 停止并删除旧容器 → 7. 拉取新镜像 → 8. 启动新容器(配置端口/重启策略)
→ 9. 验证容器状态 → 10. 清理旧镜像 → 完成

核心代码片段

获取容器信息

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('|')
    return {
      name: containerName,
      image,
      version: image.split(':')[1] || 'latest',
      status,
      ports: JSON.parse(portsJson)
    }
  } catch { return null }
}

拉取镜像与启动容器

async function pullImage(conn, config, version) {
  const fullImageName = `${config.registry}/${config.namespace}/${config.imageName}:${version}`
  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}`)
}

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}`
  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帮助信息-

使用示例

# 基础部署
node deploy/index.js

# 指定版本+强制部署
node deploy/index.js --version 2.0.0 --force

# ARM64 服务器部署
node deploy/index.js --platform linux/arm64 --host 192.168.1.100

NPM Scripts 集成

{
  "scripts": {
    "deploy": "node deploy/index.js",
    "deploy:force": "node deploy/index.js --force"
  }
}

安全与日志

敏感信息管理

  1. 配置文件(.gitignore 排除 config.json
  2. 环境变量注入
  3. 生产环境优先使用 SSH 密钥认证:
conn.connect({
  privateKey: require('node:fs').readFileSync('/path/to/private/key'),
  // 其他配置
})

彩色日志系统

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}`)
}

常见问题排查

问题类型解决方案
SSH 连接超时检查服务器地址/端口、防火墙、SSH 服务状态
Docker 命令失败安装 Docker 并启动服务(systemctl start docker
镜像拉取未授权验证仓库凭证,手动登录测试
容器启动失败检查端口占用、查看容器日志(docker logs <name>
版本检测失败安装 skopeo/jq,或手动指定版本

服务器环境配置

# 安装依赖工具
yum install -y skopeo jq curl  # CentOS
apt install -y skopeo jq curl  # Ubuntu

# 验证环境
docker --version
skopeo list-tags docker://registry.example.com/namespace/image-name

性能优化

  1. 自动清理旧镜像,释放磁盘空间
  2. 支持 AMD64/ARM64 多架构部署
  3. 容器重启策略(--restart unless-stopped)确保服务可用性

扩展功能建议

  • 健康检查:添加接口连通性检测
  • 回滚功能:支持指定历史版本部署
  • 多服务器部署:批量操作多节点
  • Webhook 通知:部署结果推送钉钉/企业微信
  • 日志持久化:部署日志写入文件归档

CI/CD 集成

GitHub Actions 示例

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' }
      - 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 示例

deploy:
  stage: deploy
  only: [tags]
  script:
    - cd cmeph-front-h5
    - pnpm install
    - node deploy/index.js --version $CI_COMMIT_TAG
  environment: { name: production }

总结

该方案具备智能版本检测、全流程自动化、灵活配置、安全可靠等特点,适用于前端应用、微服务的容器化部署,可无缝集成 CI/CD 流程。未来可扩展 Docker Compose 支持、蓝绿部署、K8s 适配等功能,进一步提升部署灵活性与稳定性。

参考资源

  • Docker 官方文档
  • ssh2 库文档
  • Skopeo 工具手册
  • Docker Registry HTTP API V2 规范

Comment