Communityコーディング&開発github.com

shared-server-docker-deploy

Node.jsアプリ(ネイティブ依存関係やChromiumを含む)を、他のアプリケーションが既に稼働しているDockerサーバーに、既存のサービスを停止させることなくデプロイします。ビルド前のディスク解放、サービスごとのメモリ制限、HTTPサービスのみのヘルスチェック、Nodeバージョンに応じたネイティブモジュールの管理、Basic認証付き共有Caddyプロキシでの公開、ダウンタイム無しのプロキシバリデーション/リロードをカバーします。他のスタックが稼働中の既存サーバーにデプロイする場合、ビルドがディスク容量不足で失敗する場合、ネイティブモジュールがプロセスを異常終了させる場合、既存のプロキシに新しいサービスを公開する必要がある場合に使用します。

shared-server-docker-deploy とは?

このスキルは、他のアプリケーションが既に稼働している共有Dockerサーバーに、Node.jsアプリケーション(ネイティブ依存関係やChromiumを含む)を安全にデプロイするためのものです。主な目的は、新しいアプリケーションのデプロイによって既存のサービスに悪影響を及ぼさないようにすることです。具体的には、ビルド前にディスク容量を確認・解放する手順、サービスごとのメモリ制限の設定、HTTPサービスのみにヘルスチェックを適用する方法、Node.jsのメジャーバージョンに応じたネイティブモジュールの互換性確認、Basic認証付きの既存Caddyリバースプロキシへの統合、そしてダウンタイム無しでのプロキシ設定のバリデーションとリロード方法を提供します。また、重い処理(Chromiumなど)をAPIサーバーとは別のワーカーコンテナに分離し、メモリとディスクを適切に制限することで、システム全体の安定性を確保します。Windows環境からのデプロイには、`tar`とSSHを利用したクロスプラットフォームな手法を推奨しています。

対応~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に質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

共有Dockerサーバーへのデプロイ

使用すべきタイミング

  • サーバーが専有ではない:既に他の人のデプロイや他のプロジェクトが動作している。
  • アプリにネイティブ依存関係(better-sqlite3、sharp、canvas)またはブラウザ(Chromium)がある。
  • 複数のドメインを公開するリバースプロキシ(Caddyやnginx)が既に存在する。
  • EADDRINUSE、OOM、またはディスク容量不足が他のスタックに影響を与える可能性がある。

基本ルール:新しいアプリが他のアプリをダウンさせる原因になってはいけない。 そのため、すべてを事前に制限し、計測します。


0. ビルド前の準備:ディスクの計測と解放

Chromiumを使ったビルドは約2GBのイメージをダウンロードします。ディスク使用率が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はワーカーが生きているかどうかを知る必要はありません。

ヘルスチェックはHTTPを提供するサービスのみに定義します。 Dockerfileからワーカーにヘルスチェックを継承すると、ワーカーはポートを開かないため、常に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を使用します;64MBでは動作しません
  tmpfs:
    - /tmp:size=256m
  security_opt:
    - no-new-privileges:true

さらに、コード内で作業量を減らします:スクレイパーはサーバー上ではローカルよりも低いDETAIL_MAX / MAX_PAGESを設定する必要があります。もしサーバーでRAMが不足した場合、原因はほとんどの場合その数値であり、Dockerではありません。

実際の検索の前後で計測:

docker stats --no-stream --format '{{.Name}}\t{{.MemUsage}}\t{{.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}}'

ファイルをホスト上で編集します:バインドマウントが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. リロード(ダウンタイムなし)

validateが失敗した場合、バックアップを復元します。リロードは既存の接続を切断しません。

Basic認証

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を使用する場合、Cloudflareプロキシを前面に使用しないでください:100秒のタイムアウトとバッファリングにより進行が中断されます。DNS-only(グレークラウド)にすると、オリジンが実際の証明書を発行できるようになります。
  • basic_authはプロキシを通過するトラフィックのみを保護します。アプリが別のポートでも公開されている場合、保護されないままになります:これをドキュメントに明記してください。

6. Windowsからのデプロイ

rsyncはWindowsには存在しません。Windowsでも動作するクロスプラットフォームな方法は、SSHパイプ経由のtarです:

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上でpnpm(.cmdファイル)を解決するには、shell: trueを指定したspawnが必須です。ただし、この場合、出力をリダイレクトするためのファイルハンドルを渡すことはできません:環境変数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.

関連スキル