skills/deploy-server/SKILL.md

```
name: deploy-server
description: Use this skill to provision or update a Welley server on a supported cloud (currently DigitalOcean) when the user asks to deploy a new instance or run a server update. Covers single-instance deployments and SSH-based updates, including interactive collection of missing credentials, SSH keys, domain, region, and sizing.
```

# 서버 배포

## 개요

이 스킬은 DigitalOcean에 단일 인스턴스 Welley 스택을 만들고, SSH로 업데이트도 실행해요. 에이전트는 빠진 입력값을 모으고, 정해진 출처에서 사용 가능한 인스턴스 사양을 가져오고, VM을 만들고, 호스트를 설정하고, `wlycli deployment create`로 배포한 뒤 결과를 확인해요.

명령 이름은 CLI와 똑같아야 해요.

-   `wlycli deployment create`
-   `wlycli deployment update`
-   `wlycli deployment delete`

## 기본값 (DigitalOcean)

-   리전: `nyc3`
-   이미지: `latest LTS available via API` (`images?type=distribution`로 확인해요)
-   Droplet 이름: `wlsly-<adjective>-<surname>` 형식으로 무작위 생성해요 (예: `wlsly-bright-hopper`). `references/digitalocean.md`의 이름 생성 스니펫을 사용하세요. 관리형 DB를 만들 때도 같은 이름을 써요.
-   플랜 목표: Basic / Regular (공유 CPU)
-   사양 목표: 2 vCPU / 4 GB RAM / 80 GB SSD / 4 TB 전송량 (월 약 $24)

## 기본값 변경 규칙

사용자가 기본값(리전, 이미지, Droplet 이름, 프로젝트, 사양) 중 하나라도 직접 지정하면 에이전트는 다음 순서를 따라야 해요.

1.  지정한 내용을 그대로 다시 말해 줘요.
2.  진행하기 전에 확인을 받아요.

## 대화형 입력 템플릿

빠진 것만 물어보세요. 헷갈리지 않도록 아래 질문을 그대로 사용해요. 여러 차례로 나눠서 물어보세요. 1차에서는 한 번에 최대 2개까지 질문해요. 2차에서는 기본값을 한꺼번에 받아들이는 “모든 기본값 수락” 선택지를 보여줄 수 있어요. 실행 환경이 선택지를 지원하면 질문마다 2~3개의 선택지(A/B/C)를 보여주세요. 꼭 필요한 경우가 아니면 자유 입력은 피해요.

1차 질문 (인증 정보와 접근 권한):

1.  “사용할 DigitalOcean API 토큰이 있나요, 아니면 토큰을 주실 때까지 기다릴까요?”
2.  “이 서버에 어떤 도메인을 쓸까요 (예: `super-puper.example.com`)? 해당 도메인의 DNS를 직접 관리하나요?”
3.  “사용할 SSH 공개 키가 이미 있나요, 아니면 제가 만들어서 업로드할까요?”
4.  “이 Droplet을 기존 DigitalOcean 프로젝트에 넣을까요? 그렇다면 프로젝트 이름이나 ID를 알려주세요.”
5.  “DNS 요구 사항: `<domain>`과 `storage.<domain>`(CDN을 쓰지 않는 경우)에 A/AAAA 레코드를 만들어야 해요. certbot을 실행하기 전에 만들어 주실 수 있나요?”
6.  “이메일 DNS 요구 사항: `--smtp-from` 도메인에 SPF, DMARC, DKIM 레코드가 있어야 해요. 없으면 새 수신자에게 가는 초대 및 확인 이메일이 조용히 버려질 수 있어요. 이미 설정되어 있나요, 아니면 SMTP 제공업체에서 요구하는 레코드를 정리해 드릴까요?”
7.  사용자가 DO 토큰을 어디서 받는지 물으면 다음 링크를 알려주세요.

```
https://cloud.digitalocean.com/account/api/tokens
```

2차 질문 (사양, 위치, 배포 옵션):

1.  “기본 리전인 `nyc3`를 쓸까요, 아니면 다른 리전을 쓸까요?”
2.  “기본 사양(2 vCPU / 2 GB)을 쓸까요, 아니면 다른 CPU/RAM 목표를 쓸까요?”
3.  “API에서 확인되는 최신 Ubuntu LTS를 쓸까요, 아니면 특정 Ubuntu 버전을 쓸까요?”
4.  “초기 설정 후 사용할 SSH 사용자는 무엇으로 할까요 (기본값: `wlsly`)? 기본값을 그대로 써도 돼요.”
5.  “관리형 Postgres DB를 쓸까요, 아니면 함께 제공되는 Postgres 컨테이너를 쓸까요?”
6.  관리형 Postgres를 선택한 경우: “DB 리전과 사양은 어떻게 할까요? `nyc3`와 1 vCPU / 2 GB를 기본으로 할 수 있어요.”
7.  관리형 Postgres를 선택한 경우: “제공업체에 표시된 월 가격으로 생성을 진행할까요?”
8.  “필수 SMTP 설정(host, user, password, from)을 알려주세요. OTEL은 선택 사항이니 필요할 때만 알려주세요.”
9.  “create에는 관리자 이메일이 필요해요. 지금 알려주세요 (플래그: `--email` / `-e`). 알려주지 않으면 확인을 위해 두 번 입력받을게요.”

입력 수집을 빠르게 하려면 2차에서 “모든 기본값 수락” 선택지를 보여주세요.

답이 빠진 항목이 있으면 한두 가지만 콕 집어서 다시 물어보세요.

## 작업 순서 (DigitalOcean, 단일 인스턴스)

1.  **필수 입력값을 모아요 (빠진 것은 물어봐요).**

-   DigitalOcean API 토큰
-   프로젝트 이름 또는 ID (선택. 없거나 찾을 수 없으면 기본 프로젝트를 써요)
-   리전
-   인스턴스 사양 목표 (예: 4 vCPU / 16 GB RAM)
-   도메인 이름
-   SSH 공개 키 (또는 새로 만들어 등록해도 된다는 확인)
-   첫 로그인과 비밀번호 복구에 쓸 관리자 이메일 (create에 필수, `--email` / `-e`). 없으면 두 번 입력받아 일치하는지 확인해요.
-   선택: 직접 준비한 TLS 인증서 또는 certbot
-   DNS 접근 확인 (`<domain>`과, 쓰는 경우 `storage.<domain>`에 A/AAAA 레코드를 만들 수 있어야 해요)

1.  **인스턴스 유형을 가져와 사양을 골라요.**

-   `references/digitalocean.md`의 사양 스니펫으로 목록을 가져와 CPU/RAM으로 걸러요.
-   맞는 후보가 여러 개면 사용자에게 확인해요.

2b. **Ubuntu 이미지 슬러그를 확인해요.**

-   `GET /v2/images?type=distribution&per_page=200`을 호출해 Ubuntu만 걸러요.
-   사용자가 특정 버전을 요청했다면 해당 슬러그가 있는지 확인하고 사용자에게 확인을 받아요.
-   그렇지 않으면 목록에서 최신 LTS 슬러그를 고르고, 생성하기 전에 사용자에게 확인을 받아요.

1.  **Droplet을 만들어요.**

-   DigitalOcean API를 직접 사용해요 (doctl은 선택 사항이에요).
-   이미지: 사용자가 지정한 것을 우선하고, 없으면 API 목록에서 최신 LTS를 골라요.
-   SSH 키 지문을 연결하거나, 키를 만들어 업로드해요.
-   모니터링을 켜요.
-   프로젝트 이름/ID를 받았지만 찾을 수 없으면 프로젝트 없이(기본 프로젝트) Droplet을 만들어요.
-   참고: 관리형 DB 생성은 Droplet 생성과 동시에 진행할 수 있어요.

## 병렬 처리 안내

에이전트는 서로 관련 없는 작업을 겹쳐서 실행해 전체 소요 시간을 줄여야 해요.

-   사용자 입력을 받는 중(2차)이라도 토큰, 리전, 사양, 이미지, SSH 키가 이미 확정되어 있으면 Droplet 생성을 시작해요.
-   Droplet을 만드는 동안 관리형 Postgres 생성을 시작해요 (요청한 경우).
-   Droplet이 시작되는 동안 남은 입력값(SMTP, OTEL, 관리자 이메일)을 계속 받아요.
-   어떤 단계에 꼭 필요한 입력이 빠져 있으면 그 단계만 멈추고, 병렬로 할 수 있는 다른 작업을 이어가요.
-   DNS를 DigitalOcean에서 관리한다면 Droplet IP를 알게 되는 즉시 A/AAAA 레코드를 만들거나 수정해요 (전체 배포 단계를 기다리지 않아요).

1.  **DNS / 도메인.**

-   DigitalOcean에서 DNS를 만들거나 수정할지, 외부 DNS를 그대로 둘지 물어봐요.
-   DigitalOcean DNS를 쓴다면 Droplet IP를 가리키는 A/AAAA 레코드를 만들어요.

1.  **SSH로 호스트를 준비해요.**

-   root가 아닌 사용자(예: `wlsly`)를 만들고, sudo 권한을 주고, SSH authorized keys를 복사해요.
-   **다음 단계로 가기 전에 root에서 나와 root가 아닌 사용자로 다시 로그인하세요.**
-   Docker와 docker compose를 설치해요.
-   AppStore에서 Welley CLI 바이너리를 받아 실행 가능하게 만들어요.
    -   기본 URL: `https://appstore.allroads.social/v1/social.wellesley.wlycli/latest?develop=stable&platform=<os>/<arch>`
    -   OS와 아키텍처에 맞는 `platform` 값을 고르세요.
        -   Linux x86\_64: `linux/amd64`
        -   Linux arm64: `linux/arm64`
        -   macOS x86\_64: `darwin/amd64`
        -   macOS arm64: `darwin/arm64`
        -   Windows x86\_64: `windows/amd64`
    -   예시 (Linux x86\_64):
        
        ```
        curl -fL 'https://appstore.allroads.social/v1/social.wellesley.wlycli/latest?develop=stable&platform=linux/amd64' -o wlycli
        chmod +x wlycli
        ```
        
    -   응답 헤더에 다음 항목이 들어 있어야 해요.
        -   `x-artifact-display-name: Welley CLI`
        -   `x-artifact-full-name: social.wellesley.wlycli`
        -   `x-artifact-kind: cli`
        -   `x-artifact-platform: <os>/<arch>` (예: `linux/amd64`)
        -   `x-artifact-sha256: <sha256>`
        -   `x-artifact-version: <version>`
        -   `x-channel: develop`
    -   설치하기 전에 HEAD 요청으로 헤더를 먼저 확인하고, 아티팩트 이름, 종류, 플랫폼, 채널이 요청한 바이너리와 일치하는지 확인하는 것이 좋아요.
-   CLI 바이너리를 확인해요. `wlycli -v`와 `wlycli deployment --help`를 실행해 예상한 빌드 정보가 맞는지 봐요.

1.  **Welley를 배포해요.**

-   필수 옵션과 함께 `wlycli deployment create`를 실행해요.
-   단일 인스턴스 기본값을 써요 (사용자가 요청하지 않으면 외부 Postgres는 쓰지 않아요).
-   관리형 Postgres를 요청받았다면 먼저 만들고(DigitalOcean DB), 반환된 host/port/db/user/password를 `--postgres-*` 플래그에 사용해요.
-   API나 UI에서 DB 사용자 비밀번호를 알려주지 않으면, `wlycli`를 실행하기 전에 DB 사용자 인증 정보를 초기화해서 새 자격 증명을 받으세요.
-   비대화형 실행에는 반드시 `--email` / `-e`를 넘겨야 해요. 그렇지 않으면 명령이 실패해요.
-   `--base-dir`은 상위 디렉터리를 가리켜요. 프로젝트 하위 디렉터리는 CLI가 자동으로 만들어요.
-   프로젝트 디렉터리나 컨테이너가 이미 있으면 CLI가 실행을 거부해요. 다시 시도하기 전에 정리해야 해요.

1.  **인증서 (필수).**

-   기본값: Certbot standalone을 사용해요 (비대화형).
-   **certbot을 실행하기 전에 사용자가 도메인(쓰는 경우 `storage.<domain>` 포함)의 A/AAAA 레코드를 Droplet IP로 지정해 두어야 해요.**
-   CLI는 `--public-ip`(감지된 IP를 덮어써요)를 지원하고, 검사를 꼭 건너뛰어야 한다면 `--skip-dns-preflight`도 지원해요.
-   **DNS가 설정되어 있다고 절대 가정하지 마세요. 물어보고 분명한 확인을 받을 때까지 기다리세요.**
-   **사용자가 명시적으로 요청하지 않으면 `--skip-dns-preflight`를 절대 쓰지 마세요.**
-   CDN을 설정하지 않았다면 certbot 인증서 요청에 `storage.<domain>`을 꼭 포함해야 해요.
-   자체 서명 인증서는 사용자가 명시적으로 요청할 때만 쓰고, `--self-signed-certs`를 넘겨요 (Go로 자체 서명 인증서를 만들어요).
-   사용자에게 DNS 설정 방법을 안내하고 확인을 기다리세요.
-   “`<domain>`이 Droplet IP로 연결되도록 A/AAAA 레코드를 만들어 주세요.”
-   “`storage.<domain>`을 쓴다면 그것도 A/AAAA 레코드를 만들어 주세요.”
-   “DNS가 반영되면 알려주세요. 그러면 이어서 진행할게요. 확인해 달라고 요청하시지 않으면 DNS 반영 여부를 제가 볼 수 없어요.”
-   에이전트가 `ping -c 1 <domain>`으로 DNS 반영을 직접 확인하고 IP를 Droplet과 비교해도 돼요. 일치하면 진행해요.

7b. **이메일 DNS (SPF / DMARC / DKIM).**

-   SPF와 DMARC가 없는 도메인에서 보낸 메일은, SMTP 제공업체에서는 전달됐다고 나와도 처음 받는 사람의 Gmail이나 Outlook에서 버려지는 경우가 많아요.
-   `--smtp-from` 도메인에 SPF 레코드가 없거나, `_dmarc.<domain>` 또는 상위 도메인에 DMARC 레코드가 없으면 `deployment create`가 `Warning:`을 출력해요. 배포를 막지는 않아요. 이 경고를 절대 무시하지 말고, 사용자에게 보여주고 추가할 레코드를 알려주세요.
    -   `<mail-domain>` TXT `v=spf1 include:<provider> ~all` (정확한 include 값은 SMTP 제공업체에서 알려줘요. 기존 SPF 레코드가 있으면 거기에 합치고, 두 번째 레코드를 추가하지 마세요)
    -   `_dmarc.<mail-domain>` TXT `v=DMARC1; p=none; rua=mailto:dmarc@<mail-domain>`
    -   DKIM: 도메인을 인증할 때 SMTP 제공업체가 만들어 주는 `<selector>._domainkey.<mail-domain>` 레코드
-   `dig +short TXT <mail-domain>`과 `dig +short TXT _dmarc.<mail-domain>`으로 확인해요.
-   CLI는 DKIM을 확인할 수 없어요 (selector가 제공업체마다 달라요). `create`가 경고를 출력하지 않았더라도 SMTP 제공업체에서 DKIM이 인증되었는지 꼭 사용자에게 확인하세요. Amazon SES 같은 제공업체는 SPF를 제공업체 자체의 envelope 도메인 기준으로 검사하기 때문에, DMARC를 통과시킬 수 있는 것은 DKIM뿐이에요. SES에서는 _Verified identities_에서 `<mail-domain>`을 열었을 때 DKIM 항목이 _Successful_로 표시되어야 해요.

1.  **확인하고 결과를 알려요.**

-   `docker ps`와 서비스 상태 확인 엔드포인트를 확인해요.
-   최종 접속 정보와 다음 단계를 알려주세요.

## SSH 업데이트 과정

사용자가 “서버 업데이트”를 요청하면 다음 순서로 진행해요.

1.  대상 호스트/IP와 SSH 사용자/키를 물어봐요.
2.  `wlycli deployment update --domain <domain> [--force-restart]`를 실행해요.
3.  컨테이너와 Nginx 상태를 확인해요.

## 참고 자료

-   DigitalOcean API 스니펫과 예시: `references/digitalocean.md`

## 비밀 값과 플래그

비밀 값에는 다음 플래그 이름을 사용해요.

-   SMTP host: `--smtp-host`
-   SMTP user: `--smtp-user`
-   SMTP password: `--smtp-password`
-   SMTP from: `--smtp-from`
-   OTEL endpoint: `--otel-endpoint`
-   OTEL headers: `--otel-headers`

## 최소 create 명령

필수 플래그:

-   `--domain`
-   `--email`
-   `--smtp-host`
-   `--smtp-user`
-   `--smtp-password`
-   `--smtp-from`

최소 예시:

```
wlycli deployment create \
  --domain example.com \
  --email admin@example.com \
  --smtp-host smtp.example.com \
  --smtp-user user \
  --smtp-password pass \
  --smtp-from noreply@example.com
```

## 관리형 Postgres 기본값 (DigitalOcean과 CLI 비교)

DigitalOcean 관리형 Postgres를 쓸 때는 CLI 기본값에 기대지 말고 `--postgres-*` 플래그를 모두 직접 지정하세요.

-   DO의 일반적인 기본값:
    -   포트: `25060`
    -   데이터베이스: `defaultdb`
    -   사용자: `doadmin`
-   CLI 기본값 (플래그를 생략했을 때):
    -   포트: `5432`
    -   데이터베이스: `wlsly`
    -   사용자: `wlsly`

## CLI 사용 참고

-   버전 확인: `wlycli -v` 또는 `wlycli --version` (플래그예요). `version` 하위 명령은 없어요.
-   `--base-dir`은 기본이 아닌 위치를 쓰고 싶을 때만 사용하세요 (기본값은 `~/.welley`). 권한이 있으면 CLI가 디렉터리를 만들어요.
-   비대화형 SSH 실행에는 `--email`과 필요한 플래그를 모두 넣어서 질문이 나오지 않게 하세요.
-   사용자가 서버에 CLI 바이너리를 직접 두었다면(예: `~/wcli`) 그대로 사용하세요. 요청이 없으면 옮기지 마세요.
-   DO API를 호출할 때는 `references/digitalocean.md`의 **스니펫을 그대로 복사**하세요. Python 파싱 부분을 고쳐 쓰거나 바꾸거나 즉흥적으로 만들지 마세요. 이스케이프나 따옴표 같은 작은 변경도 셸에 따라 깨질 수 있어요.
-   **SSH 원격 스크립트:** 항상 `ssh host 'bash -s' << 'EOF'`(따옴표를 붙인 heredoc)를 사용하세요. 큰따옴표를 쓰는 `ssh host "..."`는 쓰지 마세요. zsh가 원격 호스트로 보내기 전에 `$(...)`와 `[...]`를 로컬에서 먼저 해석해 버려요.

## 실패 후 복구

`deployment create`가 실패해서 다시 시도해야 한다면:

-   `wlycli deployment delete --domain <domain> --yes-please-delete-all-the-data`로 정리하세요.
-   delete는 root 소유 파일을 컨테이너를 이용해 정리하려고 시도해요. 그래도 실패하면 상승된 권한으로 `--base-dir` 아래의 프로젝트 디렉터리를 지우고 다시 실행하세요.

## 사전 80번 포트 테스트 (선택)

certbot이 실패하면 다시 실행하기 전에 80번 포트에 접근할 수 있는지 먼저 테스트하세요.

```
docker run --rm -d -p 80:80 --name test-http nginx:alpine
curl -s http://<domain>:80
docker rm -f test-http
```

## 다른 문서

-   [**Welley CLI** docs/index.md](https://welley.com/ko/administrators/docs/cli/)
-   [**빠른 시작: 최소 Welley 배포** docs/quick-start.md](https://welley.com/ko/administrators/docs/quick-start/)
-   [**openai.yaml** skills/deploy-server/agents/openai.yaml](https://welley.com/ko/administrators/docs/deploy-server-openai-yaml/)
-   [**DigitalOcean 참고 자료** skills/deploy-server/references/digitalocean.md](https://welley.com/ko/administrators/docs/deploy-server-digitalocean/)
