基于 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
优点:
- 最准确,能获取所有标签
- 速度快,不需要预先拉取镜像
- 支持多种镜像仓库
要求:
- 服务器上需要安装
skopeo和jq工具
方法二: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. 安全最佳实践
- 不要将包含密码的配置文件提交到版本控制系统
- 生产环境使用 SSH 密钥认证而非密码
- 定期更新密码和密钥
- 使用最小权限原则
- 启用 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] 无法获取最新版本
解决方案:
- 在服务器上安装
skopeo和jq工具 - 手动指定版本:
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 自动化部署方案,主要特点包括:
- 智能版本检测:三层检测策略,确保准确获取最新版本
- 自动化流程:从版本检测到容器启动的全自动化
- 灵活配置:丰富的命令行参数,支持多种部署场景
- 安全可靠:完整的错误处理和安全机制
- 易于扩展:模块化设计,便于添加新功能
适用场景
- 前端应用的自动化部署
- 微服务的容器化部署
- 多环境(开发、测试、生产)的部署管理
- CI/CD 流程集成
未来改进方向
- 支持 Docker Compose 多容器部署
- 支持蓝绿部署和金丝雀发布
- 添加部署前后的钩子脚本
- 支持配置文件热重载
- 添加部署统计和监控
- 支持 Kubernetes 部署
参考资源
作者注:本文基于实际项目经验总结,所有敏感信息已脱敏处理。如有问题或建议,欢迎交流讨论。