Communitygithub.com

앱 플랫폼 퍼블리시

웹사이트나 앱을 DigitalOcean App Platform에 배포하고 공개 URL을 반환합니다. 사용자가 빌드한 것을 게시, 배포, 호스팅하거나 라이브로 전환해달라고 요청할 때 사용하세요.

앱 플랫폼 퍼블리시란 무엇인가요?

이 스킬은 개발자가 샌드박스에서 빌드하고 테스트한 웹 애플리케이션을 DigitalOcean App Platform에 배포하는 전체 워크플로우를 제공합니다. 정적 사이트(HTML/CSS/JS, Vite, Astro 등)와 서버 기반 애플리케이션(Node.js, Python 등)을 모두 지원하며, Docker나 doctl 없이 공유된 GitHub 및 DigitalOcean 자격 증명을 통해 작동합니다. 주요 단계는 앱을 배포 가능한 상태로 만들기, 사용자에게 계획 제안 및 승인 받기, GitHub에 푸시, App Platform 앱 생성, 배포 대기, 공개 URL 보고로 구성됩니다. 오류 발생 시 빌드 로그를 직접 분석하여 근본 원인을 파악하고 수정을 제안하며, 각 변경 사항에 대해 사용자의 명시적 승인을 요구합니다. 또한 Postgres 데이터베이스 통합을 지원하며, 배포 후 URL을 확인하고 사용자에게 라이브 URL과 리포지토리 URL을 제공합니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/jnaidu-do/qm-appplatform-skills/tree/main/app-platform-publish

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

App Platform에 퍼블리시하기

워크플로우: 샌드박스에서 빌드 및 테스트 → 배포 가능하게 만들기 → 계획 제안 및 승인 받기 → GitHub에 푸시 → App Platform 앱 생성 → ACTIVE 대기 → URL 확인.

2단계에서 사용자가 계획을 승인하기 전에는 절대 리포지토리를 생성하거나, 푸시하거나, 앱을 생성/업데이트/삭제하지 마십시오. "퍼블리시해 줘"와 같은 요청은 승인이 아닙니다. 샌드박스에서 빌드하고 테스트하는 것은 승인이 필요 없습니다.

절대 apps 액션 publish, docker, 또는 doctl을 사용하지 마세요. 이 컴퓨터에서는 작동하지 않습니다. 모든 것은 두 개의 공유 자격 증명을 통해 이루어지며, 코어의 자격 증명 브로커를 통해 호출합니다. 토큰을 직접 볼 수 없습니다.

모든 명령어에 하나의 명시적인 sandbox_id를 사용하고, 브로커(digitalocean, github, 또는 github-git)를 호출하는 명령어의 execute.credentials에서 자격 증명을 선택하세요. 코어는 해당 명령어에만 AGENT_CREDENTIAL_TOKEN을 제공합니다. rm -rf를 사용하지 말고, 대신 mktemp -d로 새 디렉토리를 만드세요.

컴퓨터에 설치된 것

Node 24 + npm, Python 3.12 + pip, git, gh (로그인되지 않음; 사용하지 마세요), curl, gcc, make. 없음 docker, doctl, jq, wget, unzip. JSON은 node -e 또는 python3 -c로 파싱하세요.

자격 증명 (각각 할 수 있는 작업)

자격 증명호스트허용되는 작업
digitaloceanapi.digitalocean.com/v2/apps 및 /v2/regions 아래 GET/POST/PUT/DELETE
githubapi.github.com/user 및 /repos/jnaidu-do 아래 GET/POST/PATCH
github-gitgithub.com (git push/clone)/jnaidu-do/ 아래 GET/POST

모든 것은 공유 GitHub 계정 jnaidu-do(배포 전체 자격 증명, 사용자 개인 계정이 아님) 아래에 생성됩니다. 계획에 이 내용을 명시하세요. 사용자가 자신의 계정을 원하면 아직 설정되지 않았다고 알리세요. 403 path_not_allowed / method_not_allowed 오류는 요청이 허용 범위를 벗어났음을 의미합니다. 변형을 시도하지 말고 사용자에게 허용되지 않는다고 알리세요.

브로커 헬퍼

명령어당 한 번씩 붙여넣으세요 (셸 상태는 유지되지 않습니다). HTTP 상태를 stderr로, 응답 본문을 stdout으로 출력합니다:

bk() { # bk <자격증명> <메서드> <url> [json-본문]
  p=$(node -e 'const [c,m,u,b]=process.argv.slice(1);console.log(JSON.stringify({credential:c,method:m,url:u,headers:{accept:"application/json","content-type":"application/json"},...(b?{body:b}:{})}))' "$1" "$2" "$3" "${4:-}")
  curl -fsS -X POST "$AGENT_API_URL/v1/credentials/broker" -H "x-agent-capability: $AGENT_CREDENTIAL_TOKEN" \
    -H "content-type: application/json" -d "$p" |
  node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const r=JSON.parse(s);console.error("HTTP "+r.status);console.log(r.body)})'
}

1. 배포 가능하게 만들기 (푸시 전에 확인)

앱은 이 샌드박스가 아닌 App Platform 뒤의 일반 컨테이너로 실행됩니다.

  • $PORT에서 수신 대기, 0.0.0.0에 바인딩 (App Platform이 PORT를 설정하며, 기본값은 8080). 다른 포트를 하드코딩하지 마세요.
  • 일반 HTML/CSS/JS 또는 정적 파일로 빌드되는 프레임워크 (Vite, CRA, Astro): 정적 사이트로 배포하세요 (build_command, output_dir). 정적 페이지에 서버를 실행하는 것보다 이것을 선호하세요.
  • 서버가 필요한 모든 것: 서비스로 build_command, run_command, http_port와 함께 배포하세요. 표준 레이아웃은 설정 없이 작동합니다: Node는 start 스크립트가 있는 package.json이 필요하고, Python은 requirements.txt와 실행 명령어(예: gunicorn app:app --bind 0.0.0.0:$PORT)가 필요합니다. 리포지토리의 Dockerfile도 작동하지만(dockerfile_path 설정), 여기서 테스트 빌드는 할 수 없으므로 간단하게 유지하세요.
  • 영구 디스크 없음. 파일 시스템에 기록된 모든 것은 배포할 때마다 사라집니다. SQLite 파일, 디스크 업로드, 또는 상태 저장용 JSON 파일을 사용하지 마세요. 앱에 데이터베이스가 필요한 경우 관리형 Postgres를 추가하고(아래 참조), 그렇지 않으면 상태를 유지하지 않도록 하세요.
  • 먼저 로컬에서 실행: npm install, PORT=8080으로 시작하고 curl localhost:8080으로 확인하세요. 작동하는 것만 퍼블리시하세요.
  • node_modules 등이 포함된 .gitignore를 추가하세요. 시크릿을 커밋하지 마세요. 토큰을 절대 출력하지 마세요.

앱이 Postgres를 사용하는 경우

App Platform의 ${db.DATABASE_URL}은 sslmode=require를 포함하며, Node pg는 관리형 CA를 거부합니다(SELF_SIGNED_CERT_IN_CHAIN). URL에서 sslmode를 제거하고 ssl: { rejectUnauthorized: false }도 설정하세요:

function normalizeDatabaseUrl(raw) {
  const u = new URL(raw);
  u.searchParams.delete("sslmode"); u.searchParams.delete("ssl");
  return u.toString();
}
const pool = new Pool({ connectionString: normalizeDatabaseUrl(process.env.DATABASE_URL), ssl: { rejectUnauthorized: false } });

spec에 "databases": [{"name":"db","engine":"PG","production":false,"version":"16"}]를 추가하고, {"key":"DATABASE_URL","scope":"RUN_TIME","value":"${db.DATABASE_URL}"}를 데이터베이스가 필요한 각 컴포넌트에 추가하세요. 첫 번째 연결을 약 30초 동안 재시도하세요: 데이터베이스가 아직 시작 중일 수 있습니다. 앱이 실제로 필요할 때만 데이터베이스를 추가하세요(추가 비용이 발생합니다).

2. 계획 제안 및 승인 요청 (사용자가 동의할 때까지 여기서 멈춤)

앱이 빌드되고 로컬에서 실행되면, 멈추고 사용자에게 간단한 계획을 보낸 후 답변을 기다리세요. 사용자가 승인할 때까지 bk 호출, git push, 리포지토리/앱 생성을 실행하지 마세요. 다음을 포함하세요:

  1. 빌드한 내용과 로컬에서 실행되었다는 사실 (한 줄)
  2. 수행할 단계, 순서대로, 각 단계가 무엇에 영향을 미치는지:
    • GitHub 리포지토리 jnaidu-do/<이름> 생성 — 공개, 공유 jnaidu-do 계정 아래 (사용자 개인 계정이 아님)
    • git으로 앱 파일 푸시 (포함된 내용 목록, 시크릿 없음 확인)
    • nyc 리전에 App Platform 앱 <이름> 생성: 정적 사이트 (소수는 무료) 또는 서비스 (basic-xxs, 월 약 $5) — 어떤 종류와 비용을 명시
    • 빌드 대기 (3-5분), 그 후 공개 URL 제공
  3. 공개되는 것: 리포지토리와 라이브 사이트는 인터넷의 누구나 읽을 수 있습니다.
  4. 다시 묻지 않고 하지 않을 것: 추가 변경 사항 푸시, spec 편집, 또는 삭제.

직접적인 질문으로 끝내세요, 예: "위와 같이 리포지토리와 앱을 생성할까요? 이름을 변경하거나, 리포지토리를 비공개로 만들거나(DO GitHub App 권한 필요), 거절할 수도 있습니다." 관련된 대안(정적 vs 서비스, 다른 이름)을 제시하세요. 거절하면 파일이나 리포지토리 준비 코드를 제공하고 중단하세요.

이후의 git push (업데이트), 앱 spec 변경, 앱 삭제 전에도 각각 별도로 다시 물어보세요.

3. GitHub에 푸시

2단계에서 제안한 이름(qm-<짧은-케밥-이름>)을 사용하세요 (소문자). 이름이 이미 사용 중이면(422) 짧은 접미사를 추가하세요.

# 리포지토리 생성 (공개: App Platform이 추가 인증 없이 클론 가능)
bk github POST https://api.github.com/user/repos '{"name":"qm-hello-site","private":false}'

# 앱이 포함된 새 깨끗한 디렉토리에서 푸시 (node_modules 없음)
cd "$(mktemp -d)" && cp -r /path/to/app/. . && git init -q -b main && git add . &&
git -c user.name=QM -c [email protected] commit -qm "Initial commit" &&
git remote add origin "$AGENT_API_URL/v1/credentials/git/github-git/jnaidu-do/qm-hello-site.git" &&
git -c http.extraHeader="x-agent-capability: $AGENT_CREDENTIAL_TOKEN" push origin HEAD:refs/heads/main

이후 업데이트: 동일한 방식으로 커밋하고 푸시하세요 (HEAD:refs/heads/main). 단, 변경 사항을 사용자에게 알리고 승인을 받은 후에만 진행하세요. 모든 푸시는 새로운 승인이 필요하며, 첫 번째 계획으로 처리되지 않습니다.

4. App Platform 앱 생성

bk digitalocean POST https://api.digitalocean.com/v2/apps '{"spec": {
  "name": "qm-hello-site",
  "region": "nyc",
  "services": [{
    "name": "web",
    "github": {"repo": "jnaidu-do/qm-hello-site", "branch": "main", "deploy_on_push": true},
    "build_command": "npm install",
    "run_command": "npm start",
    "http_port": 8080,
    "instance_size_slug": "basic-xxs",
    "instance_count": 1
  }]
}}'

services 대신 정적 사이트: "static_sites": [{"name":"web","github":{...},"build_command":"npm run build","output_dir":"dist"}] (일반 HTML: build_command 생략, output_dir은 "/"; 단일 페이지 앱의 경우 "catchall_document":"index.html" 추가).

응답에 앱 id가 포함됩니다. 보관하세요. 앱 이름은 소문자, 숫자, 대시만 가능하며 2-32자입니다.

GitHub 리포지토리 액세스 오류로 생성이 실패하는 경우 ("리포지토리에 액세스할 수 없습니다"): DigitalOcean GitHub App이 이 리포지토리에 대해 권한을 부여받지 못한 것입니다. 사용자에게 https://github.com/apps/digitalocean/installations/select_target을 열고 jnaidu-do 리포지토리에 대한 액세스를 승인한 후 완료했다고 알리라고 하세요. 동일한 요청을 재시도하세요. 사용자가 할 수 없으면 일반 공개 클론 소스로 대체하세요("git": {"repo_clone_url": "https://github.com/jnaidu-do/qm-hello-site.git", "branch": "main"}). 이후 업데이트는 재배포가 필요하다고 알리세요(아래 참조).

5. ACTIVE 대기

실제 빌드는 3-5분이 소요됩니다. 30초마다 폴링하고 사용자에게 기다리고 있다고 알리세요:

bk digitalocean GET "https://api.digitalocean.com/v2/apps/$APP_ID/deployments?per_page=1" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const d=JSON.parse(s).deployments[0];console.log(d.id,d.phase,d.progress?.success_steps+"/"+d.progress?.total_steps)})'

단계: PENDING_BUILD → BUILDING → DEPLOYING → ACTIVE. ERROR 또는 CANCELED는 실패를 의미합니다. "실패 시" 섹션으로 이동하세요. 약 12분 후 폴링을 중단하고 사용자에게 현재 상태를 알리세요.

6. URL 보고

bk digitalocean GET "https://api.digitalocean.com/v2/apps/$APP_ID" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>console.log(JSON.parse(s).app.live_url))'

curl -s -o /dev/null -w '%{http_code}' <live_url>로 시도하세요. 200이 예상됩니다. 이 컴퓨터는 새 *.ondigitalocean.app 사이트에 종종 연결할 수 없습니다 (연결 실패). curl이 실패했지만 배포가 ACTIVE인 경우, 배포 실패가 아닙니다: 여기서 확인할 수 없었다고 말하고 사용자에게 URL을 열어보라고 요청하세요. 사용자에게 전체 https://…ondigitalocean.app URL과 리포지토리 URL을 제공하세요. 앱은 인터넷에 공개됩니다.

실패 시

로그를 직접 읽으세요. 사용자에게 요청하지 마세요. 로그 URL을 가져와 가져오세요:

bk digitalocean GET "https://api.digitalocean.com/v2/apps/$APP_ID/deployments/$DEPLOYMENT_ID/logs?type=BUILD"
# 본문에 "live_url"(또는 "historic_urls")이 있습니다. 해당 URL을 curl로 가져오세요 (인증 불필요)

type은 BUILD, DEPLOY, 또는 RUN입니다 (정적 사이트는 BUILD만 있음). 신호와 일치시킵니다:

신호수정
Missing script: start / 명령어를 찾을 수 없음잘못된 build_command / run_command, 또는 start 스크립트 없음
컨테이너 종료, 상태 확인 실패$PORT에서 수신 대기 중이지 않음 / 0.0.0.0에 바인딩되지 않음, 또는 부팅 시 충돌
SELF_SIGNED_CERT_IN_CHAINPostgres SSL 헬퍼 누락 (위 참조)
Cannot find modulepackage.json에 종속성 누락, 또는 커밋된 node_modules 불일치
Vite "Blocked request. This host is not allowed"preview: { allowedHosts: true } 설정 (또는 빌드된 dist를 정적 사이트로 제공)
빌드 "건너뜀" / 몇 초 만에 ACTIVE소스가 변경되지 않음. 실제 커밋 푸시

사용자에게 근본 원인(정확한 로그 라인)과 제안하는 수정 방법을 알리고, 수정 사항을 푸시하거나 spec을 변경하기 전에 승인을 요청하세요 — 각 푸시와 spec 변경에는 각각의 승인이 필요합니다. 승인 후 코드를 수정하고, 커밋 및 푸시하면 (deploy_on_push가 자동으로 재배포) 다시 폴링합니다. spec 변경의 경우 PUT /v2/apps/$APP_ID에 {"spec": {...}}를 보내세요 (먼저 GET /v2/apps/$APP_ID로 현재 spec을 가져와 편집). 일반 git 소스의 경우 POST /v2/apps/$APP_ID/deployments 본문 {"force_build":true}로 강제 재빌드하세요. 실패를 수정하기 위해 앱을 삭제하고 다시 생성하지 마세요. URL이 변경됩니다. 실패를 해결하세요. 세 번의 서로 다른 시도가 실패하면 시도한 내용을 보고하고 포기하세요. 로그를 읽는 것은 승인이 필요 없습니다.

삭제

사용자가 명시적으로 요청할 때만, 먼저 앱 이름을 언급하세요. DELETE /v2/apps/$APP_ID는 앱을 제거합니다. GitHub 리포지토리는 남겨집니다 (이 자격 증명은 리포지토리를 삭제할 수 없음). 그 사실을 알리세요.

보고

다음으로 끝내세요: 라이브 URL, 리포지토리 URL, 업데이트 방법에 대한 한 줄 ("X를 변경해 달라고 요청하시면 푸시하겠습니다").

관련 스킬