Community编程与开发github.com

shared-server-docker-deploy

在已有其他应用的 Docker 服务器上部署 Node 应用(含原生依赖或 Chromium),不影响现有服务。涵盖:构建前释放磁盘空间、服务内存限制、仅 HTTP 服务添加健康检查、根据 Node 版本匹配原生模块版本、通过共享 Caddy 反向代理发布并配置 basic auth、无 downtime 验证和重载代理。适用于在已有多个栈的服务器上部署、构建因磁盘空间不足而失败、原生模块导致进程崩溃、或需要通过已有代理暴露新服务等场景。

shared-server-docker-deploy 是什么?

该技能提供了一整套在共享 Docker 服务器上安全部署 Node.js 应用的方案,即使应用包含原生模块(如 better-sqlite3、sharp、canvas)或 Chromium 浏览器。其核心原则是“新应用不能导致其他应用宕机”,因此在构建前会执行磁盘空间检查和清理(如使用 docker builder prune 等命令),并确保有足够的自由空间。技能详细说明了如何根据 Node.js 主版本匹配原生模块版本以避免进程崩溃,如何通过分离 API 和 Worker 进程来隔离重负载任务(如 Chromium),以及如何为每个服务设置精确的内存限制(mem_limit)。部署到已有 Caddy 或 nginx 反向代理的服务器时,技能指导如何将新服务连接到现有的边缘网络、在现有的 Caddyfile 中添加配置块(包括 basic auth 和 SSE 支持)、以及如何无 downtime 地验证和重载代理配置。此外,还涵盖了从 Windows 系统使用 tar over SSH 进行部署、构建 Docker 镜像时的最佳实践(如锁定 Playwright 版本、使用多阶段构建)、以及部署前后的检查清单和快速诊断命令。该技能适用于 Cursor、Claude Code 等支持自定义技能的平台,旨在帮助开发者安全、可靠地在共享基础设施上部署应用。

兼容平台~Claude Code~Codex CLI~Cursor
Part ofMobile
npx skills add https://github.com/leoarayas/agent-skills/tree/main/shared-server-docker-deploy

Installed? Explore more 编程与开发 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

在共享 Docker 服务器上部署

何时使用

  • 服务器非专用:已运行其他人或其他项目的部署。
  • 应用包含原生依赖(better-sqlite3、sharp、canvas)或浏览器(Chromium)。
  • 已有反向代理(Caddy 或 nginx)发布多个域名。
  • 任何 EADDRINUSE、OOM 或磁盘满问题都会影响其他栈。

黄金法则:新应用不能成为其他应用宕机的原因。 因此所有操作都经过预先限制和测量。


0. 构建前:测量并释放磁盘空间

使用 Chromium 构建会下载约 2 GB 的镜像。如果磁盘使用率达到 78%,构建会失败,或使服务器处于极限状态,下一次 prune 会删除生产镜像。

ssh SERVIDOR 'docker system df; df -h /'

通常可安全释放的闲置资源:

docker builder prune -af      # 构建缓存(通常占用最大)
docker image prune -af        # 未使用的镜像
docker container prune -f     # 已停止的容器

前后统计容器数量以确认没有服务宕机:

docker ps -q | wc -l   # 保存数字,之后对比

实际案例测量:78% → 32% 恢复,41 个生产容器完好无损。


1. 原生模块与 Node 版本

为另一个 Node 主版本编译的 .node 文件不会产生清晰的错误信息:它会在内部断言失败并导致进程崩溃,留下无用的原生堆栈跟踪。

典型症状:

# node.exe[61084]: void __cdecl node::RemoveEnvironmentCleanupHook(Isolate *, CleanupHook, void *)
# Assertion failed: (env) != nullptr
----- Native stack trace -----

原因:better-sqlite3@11 与 Node 24 不兼容。支持需要 better-sqlite3@12。

规则:

  • 在集成之前,验证原生模块版本是否与 Node 主版本兼容。
  • 在构建用的 Dockerfile 中,安装工具链(python3 make g++),以便在预编译二进制不存在时可以即时编译。
  • 运行时,不要假设需要工具链:最好使用更小的最终镜像。
  • 如果模块从工作区的多个包中导入,在一个只做此事的进程中测试:延迟的 GC 崩溃可能在数小时后才出现。

2. 分离重负载进程与响应进程

将 Chromium 和原生模块放在与 HTTP 服务器相同的进程中会导致崩溃,并且长时间任务会阻塞事件循环。

api     : HTTP + SSE, 低内存, HTTP 健康检查
worker  : 队列 + 浏览器, 高内存, 无端口, 无 HTTP 健康检查

它们通过共享存储(SQLite、Redis、Postgres)通信。API 无需知道 Worker 是否存活。

健康检查仅在提供 HTTP 服务的容器上定义。 从 Dockerfile 继承健康检查到 Worker 会导致它永远处于 unhealthy 状态,因为它从不打开端口:

worker:
  healthcheck:
    disable: true

3. 为每个服务设置内存限制

api:
  mem_limit: 400m
worker:
  mem_limit: 1600m
  mem_reservation: 400m
  volumes:
    - /dev/shm:/dev/shm:rw,size=256m   # Chromium 使用 /dev/shm; 64 MB 会导致它崩溃
  tmpfs:
    - /tmp:size=256m
  security_opt:
    - no-new-privileges:true

此外,在代码中减少工作量:爬虫在服务器上的 DETAIL_MAX / MAX_PAGES 应比本地低。如果某天服务器内存不足,罪魁祸首几乎总是这个数字,而不是 Docker。

在执行实际搜索前后进行测量:

docker stats --no-stream --format '{{.Name}}	{{.MemUsage}}	{{.CPUPerc}}' <api> <worker>
free -m | head -2

4. 构建:有效的阶段和缓存

FROM mcr.microsoft.com/playwright:v1.63.0-noble AS build   # 版本锁定到 npm 包版本
...
FROM mcr.microsoft.com/playwright:v1.63.0-noble AS runtime
  • 锁定镜像版本到已安装的 npm 包版本。如果不匹配,Playwright 找不到浏览器。
  • 先复制 package.json,再复制代码,这样 COPY . . 不会使 install 层失效。
  • 在运行时,如果启动脚本使用 pnpm/npm,则需要启用 corepack enable。
  • 如果 node_modules 从构建阶段复制过来且权限不匹配,pnpm 会在每次启动时尝试“修复”并失败。可以通过在镜像的 .npmrc 中设置 verify-deps-before-run=false 来禁用此行为。
  • 运行时使用非 root 用户。Playwright 镜像已包含 pwuser。

pnpm-lock.yaml 应提交到版本控制,并且构建时的 install 应使用 --frozen-lockfile。


5. 通过已有代理发布

如果已经有 Caddy 发布多个域名,不要启动另一个代理或打开另一个公共端口:只需在现有的 Caddyfile 中添加一个块,并将新服务加入 Docker 网络。

services:
  api:
    networks: [edge_proxy]        # 外部网络,已创建
networks:
  edge_proxy:
    external: true
    name: edge_proxy

找到网络和 Caddyfile 的位置:

docker inspect <proxy> --format '{{range .Mounts}}{{.Source}} -> {{.Destination}} rw={{.RW}}{{println}}{{end}}'

在宿主机上编辑文件:如果 bind mount 为 rw=false,在容器内写入会失败并显示 Resource busy,或者在重启后丢失。

安全流程(绝不盲目编辑):

cp "$CADDYFILE" "$CADDYFILE.bak-$(date +%Y%m%d-%H%M%S)"   # 1. 备份
# 2. 添加配置块
docker exec <proxy> caddy validate --config /etc/caddy/Caddyfile  # 3. 验证
docker exec <proxy> caddy reload  --config /etc/caddy/Caddyfile   # 4. 重载(无 downtime)

如果 validate 失败,则恢复备份。重载不会断开现有连接。

Basic auth

prospector.ejemplo.dev {
	basic_auth {
		leo $2a$14$...        # bcrypt,绝不用明文
	}
	reverse_proxy api:3000 {
		flush_interval -1     # SSE 必需
	}
	encode zstd gzip
	header {
		X-Frame-Options DENY
		-Server
	}
}
  • 在服务器上生成哈希:docker exec <proxy> caddy hash-password --plaintext '...'。
  • 仅将明文密码保存在一个 root 文件(/root/<app>-password.txt,权限 600)中,以便恢复或轮换,绝不要保存在仓库中。
  • 如果应用使用 SSE,不要在 Caddy 前面使用 Cloudflare 代理:100 秒超时和缓冲会中断进度。使用 DNS-only(灰色云)让源站颁发真实证书。
  • basic_auth 仅保护通过代理的流量。如果应用还通过其他端口暴露,则不受保护:需记录此情况。

6. 从 Windows 部署

Windows 没有 rsync。跨平台可用的方案是 tar 通过 SSH 管道:

const tar = spawn('tar', ['-cf', '-', '--exclude', './node_modules', ...excludes, '.'], { cwd: ROOT });
const ssh = spawn('ssh', [HOST, `mkdir -p ${DIR} && tar -xf - -C ${DIR}`], { stdio: ['pipe', 'inherit', 'inherit'] });
tar.stdout.pipe(ssh.stdin);

在 Windows 上,spawn 必须使用 shell: true 才能解析 pnpm(它是一个 .cmd 文件),但这样不能传递文件句柄来重定向输出:应使用环境变量 LOG_FILE,让应用自己写入日志。


7. 接触共享服务器之前的检查清单

  • 保存 docker ps -q | wc -l,最后对比
  • 释放后有足够的可用磁盘空间
  • 所有新服务设置 mem_limit
  • 重负载进程放在独立容器中,无端口
  • 仅提供 HTTP 服务的容器配置健康检查
  • 声明使用代理的外部网络,而非创建新网络
  • 备份 Caddyfile,重载前执行 caddy validate
  • 日志写入文件(进程将无控制台)
  • 部署脚本幂等:check / push / build / up / logs / stats / backup
  • 备份卷并已测试恢复,而不仅仅是创建

快速诊断

docker stats --no-stream --format '{{.Name}} {{.MemUsage}} {{.CPUPerc}}' <api> <worker>
docker logs --tail 80 <worker>
docker exec <api> node -e "fetch('http://127.0.0.1:3000/api/health').then(r=>r.text()).then(console.log)"
free -m && df -h /

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

相关技能