Community코딩 & 개발github.com

shared-server-docker-deploy

Node 앱(네이티브 의존성 또는 Chromium 포함)을 이미 다른 애플리케이션을 호스팅 중인 Docker 서버에 배포하여, 기존 서비스를 중단시키지 않습니다. 디스크 확보, 서비스별 메모리 제한, HTTP 서비스 전용 헬스체크, Node 버전에 따른 네이티브 모듈 호환성, Basic Auth가 적용된 공유 Caddy 프록시 뒷단에 게시, 다운타임 없는 프록시 검증/재로딩을 다룹니다. 다른 스택이 실행 중인 기존 서버에 배포하거나, 빌드가 디스크 공간 부족으로 실패하거나, 네이티브 모듈이 프로세스를 중단시키거나, 이미 존재하는 프록시를 통해 새 서비스를 노출해야 할 때 사용합니다.

shared-server-docker-deploy란 무엇인가요?

이 스킬은 **공유 Docker 서버**에 Node.js 애플리케이션(네이티브 모듈 또는 Chromium 포함)을 기존 서비스를 중단시키지 않고 배포하는 방법을 제공합니다. 주요 기능으로는 빌드 전 디스크 공간 확인 및 확보, 서비스별 메모리 제한(mem_limit), HTTP 서비스에만 헬스체크 적용, Node.js 메이저 버전에 맞는 네이티브 모듈(better-sqlite3 등) 버전 검증, Playwright 기반 멀티 스테이지 Dockerfile 작성, 공유 Caddy 프록시에 Basic Auth와 함께 새 서비스 추가, 그리고 `caddy validate`와 `caddy reload`를 통한 무중단 프록시 업데이트를 포함합니다. 또한 무거운 작업(worker)과 API 서버를 분리하여 리소스를 격리하고, Windows 환경에서 `tar` 파이프 SSH를 이용한 크로스 플랫폼 배포 방법, 헬스체크 비활성화, `/dev/shm` 볼륨 설정 등 Chromium 관련 세부 설정을 제공합니다. 이 스킬은 Claude Code, Cursor, Codex 등 다양한 AI 에이전트 플랫폼과 호환되며, 개발자가 안전하고 체계적으로 공용 서버에 배포할 수 있도록 돕습니다.

지원 대상~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 서버에 배포하기

사용 시기

  • 서버가 전용(dedicated)이 아닌 경우: 이미 다른 사람이나 다른 프로젝트의 배포가 실행 중입니다.
  • 앱에 네이티브 의존성(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 파일은 명확한 오류와 함께 실패하지 않습니다. 내부 assert와 쓸모없는 네이티브 스택으로 프로세스를 중단시킵니다.

일반적인 증상:

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

원인: Node 24에서 better-sqlite3@11. 지원은 better-sqlite3@12에서 제공됩니다.

규칙:

  • 통합 전에 네이티브 모듈 버전을 Node 메이저 버전과 확인합니다.
  • 빌드 Dockerfile에서 toolchain(python3 make g++)을 설치하여 사전 컴파일된 바이너리가 없을 경우 즉석에서 컴파일할 수 있도록 합니다.
  • 런타임에서는 toolchain이 필요하다고 가정하지 않습니다: 더 작은 최종 이미지가 좋습니다.
  • 모듈이 워크스페이스의 여러 패키지에서 가져오는 경우, 다른 작업을 수행하지 않는 프로세스에서 테스트하십시오: 지연된 GC로 인한 크래시는 몇 시간 후에 나타날 수 있습니다.

2. 무거운 프로세스와 응답 프로세스 분리

HTTP 서버와 동일한 프로세스에서 Chromium과 네이티브 모듈을 실행하면 크래시가 발생하고, 긴 작업 중에는 이벤트 루프가 차단됩니다.

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 값을 더 낮게 설정해야 합니다. 언젠가 서버에 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로 비활성화합니다.
  • 런타임에서 루트가 아닌 사용자. Playwright 이미지에는 이미 pwuser가 있습니다.

pnpm-lock.yaml은 커밋되어야 하며 빌드 설치는 --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 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/<app>-password.txt, 600)에만 저장하고 절대 리포지토리에 저장하지 마십시오.
  • 앱이 SSE를 사용하는 경우 앞단에 Cloudflare 프록시를 사용하지 마십시오: 100초 타임아웃과 버퍼링으로 인해 진행이 중단됩니다. DNS 전용(회색 구름)을 사용하면 원본이 실제 인증서를 발급할 수 있습니다.
  • basic_auth는 프록시를 통과하는 트래픽만 보호합니다. 앱이 다른 포트로도 노출되면 보호되지 않은 상태로 남습니다: 이를 문서화하십시오.

6. Windows에서 배포

rsync는 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 파일)을 확인하려면 spawn에 shell: true가 필수이지만, 그러면 출력 리디렉션을 위해 파일 핸들을 전달할 수 없습니다: 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.

관련 스킬