在现代 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
- 优势:准确获取全量标签、速度快、多仓库支持
- 依赖:服务器安装
skopeo和jq
方法二: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"
}
}
安全与日志
敏感信息管理
- 配置文件(
.gitignore排除config.json) - 环境变量注入
- 生产环境优先使用 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
性能优化
- 自动清理旧镜像,释放磁盘空间
- 支持 AMD64/ARM64 多架构部署
- 容器重启策略(
--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 规范