fix(deploy): webhook deployer spawn 로그 redirect + base64 quoting
Deploy momo-erp / deploy (push) Failing after 14m17s

이전: spawn(docker, [...], { stdio: 'ignore' }) → 에러 보이지 않음
이제: sh -c 'docker run -d ... >> log 2>&1' → 컨테이너 ID/에러 모두 로그
+ 내부 스크립트 base64 인코딩으로 sh -c 중첩 quoting 안전화

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
chpark
2026-05-30 16:38:17 +09:00
parent d30c8ad8d3
commit 1061332fbd
+39 -29
View File
@@ -29,36 +29,46 @@ export async function POST(req: NextRequest) {
// 배포가 도중에 끊겼던 문제를 해결. docker daemon 이 띄운 별도 컨테이너는
// momo-erp 의 PID/network 와 무관하게 살아남는다.
//
// 임시 컨테이너:
// - root(--user 0:0) 로 실행 → .git permission 문제 없음
// - 호스트 source 디렉토리와 docker.sock 마운트
// - 종료 후 자동 삭제(--rm), 로그는 /tmp/momo-deploy.log 에 누적
const deployCmd = [
"docker", "run", "-d", "--rm",
"--name", `momo-deployer-${Date.now()}`,
"-v", "/home/chpark/momo-erp/source:/src",
"-w", "/src",
"-v", "/var/run/docker.sock:/var/run/docker.sock",
"--user", "0:0",
"docker:cli",
"sh", "-c",
[
`echo "[$(date)] deployer 시작" >> ${logPath}`,
`apk add --no-cache git docker-cli-compose >/dev/null 2>&1`,
`git config --global --add safe.directory '*'`,
`git fetch origin >> ${logPath} 2>&1`,
`git reset --hard origin/main >> ${logPath} 2>&1`,
`git rev-parse HEAD > public/build-sha.txt`,
`echo "[$(date)] 배포 SHA: $(cat public/build-sha.txt)" >> ${logPath}`,
`docker compose -f docker-compose.prod.yml build momo-erp >> ${logPath} 2>&1`,
`docker compose -f docker-compose.prod.yml up -d --force-recreate momo-erp >> ${logPath} 2>&1`,
`docker restart traefik >> ${logPath} 2>&1`,
`docker image prune -f >> ${logPath} 2>&1`,
`echo "[$(date)] ✔ 배포 완료" >> ${logPath}`,
].join(" && "),
];
// - root(--user 0:0) → .git permission 무관
// - --rm 종료 후 자동 삭제
// - docker run -d 결과(컨테이너 ID 또는 에러) 와 모든 진단 로그를
// /tmp/momo-deploy.log 에 redirect → 디버깅 가능
const deployerName = `momo-deployer-${Date.now()}`;
const innerScript = [
`echo "[$(date)] deployer 내부 시작 ($(hostname))"`,
`apk add --no-cache git docker-cli-compose >/dev/null 2>&1 || echo "[WARN] apk add 실패 (이미 설치돼 있을 수 있음)"`,
`git config --global --add safe.directory '*'`,
`git fetch origin`,
`git reset --hard origin/main`,
`git rev-parse HEAD > public/build-sha.txt`,
`echo "[$(date)] 배포 SHA: $(cat public/build-sha.txt)"`,
`docker compose -f docker-compose.prod.yml build momo-erp`,
`docker compose -f docker-compose.prod.yml up -d --force-recreate momo-erp`,
`docker restart traefik || true`,
`docker image prune -f >/dev/null 2>&1 || true`,
`echo "[$(date)] ✔ 배포 완료"`,
].join(" && ");
const child = spawn(deployCmd[0], deployCmd.slice(1), { detached: true, stdio: "ignore" });
// 안전한 shell quoting: innerScript 를 base64 인코딩해서 컨테이너 안에서 디코드 실행
// (sh -c 내부에 sh -c 가 중첩되며 따옴표 escaping 이 깨지는 위험을 피함)
const innerB64 = Buffer.from(innerScript).toString("base64");
// 호스트의 docker daemon 에 -d(detached) 컨테이너 spawn 요청.
// 그 자체 명령은 1초 안에 끝나고 컨테이너 ID 반환 → momo-erp 가 죽어도 deployer 는 계속 돔.
const outerScript = `
echo "[$(date)] webhook 호출됨 — deployer spawn 시도: ${deployerName}" >> ${logPath}
docker run -d --rm \
--name ${deployerName} \
-v /home/chpark/momo-erp/source:/deploy/source \
-w /deploy/source \
-v /var/run/docker.sock:/var/run/docker.sock \
--user 0:0 \
docker:cli \
sh -c "echo ${innerB64} | base64 -d | sh" >> ${logPath} 2>&1
echo "[$(date)] deployer spawn 종료(exit=$?)" >> ${logPath}
`;
const child = spawn("/bin/sh", ["-c", outerScript], { detached: true, stdio: "ignore" });
child.unref();
return NextResponse.json({