skills/deploy-server/references/digitalocean.md

# DigitalOcean リファレンス

すべてのスニペットは `curl` + `python3` を使います (外部依存はありません)。実行前に `DO_TOKEN` を設定してください。

**シェルの互換性:** `python3 -c` の引数はすべてシングルクォートで囲んでいるので、bash でも zsh でも変数は展開されません。Python のコードではダブルクォートだけを使っています。

**DO\_TOKEN の設定:** zsh では、`DO_TOKEN=... curl ...` を1行で書いても動作しません。代入が有効になる前に `$DO_TOKEN` が展開されるためです。次のどちらかを使ってください。

```
export DO_TOKEN=...          # then run snippets normally
DO_TOKEN=...; curl ...       # semicolon separates assignment from command
```

**エラー処理:** すべてのスニペットは、データにアクセスする前に DO API のエラーを確認します。DO のエラーは `{"id": "...", "message": "..."}` の形式です。

## インスタンスのサイズ

利用できるサイズを一覧表示して絞り込みます。

```
curl -sS -H "Authorization: Bearer $DO_TOKEN" \
  "https://api.digitalocean.com/v2/sizes?per_page=200" | python3 -c '
import json, sys
data = json.load(sys.stdin)
if "sizes" not in data:
    print("Error:", data.get("message", data)); sys.exit(1)
region, vcpus, mem_gb = "nyc3", 2, 2
for s in sorted(data["sizes"], key=lambda x: (x["vcpus"], x["memory"], x["price_monthly"])):
    if not s["available"] or region not in s["regions"]:
        continue
    if s["vcpus"] == vcpus and s["memory"] == mem_gb * 1024:
        slug, vc, mem, disk, price = s["slug"], s["vcpus"], s["memory"]/1024, s["disk"], s["price_monthly"]
        print(f"{slug:30s} {vc}vCPU {mem:.0f}GB {disk}GB ${price}/mo")
'
```

必要に応じて `region`、`vcpus`、`mem_gb` を調整してください。最小値以上で絞り込みたいときは、`==` の代わりに `>=` を使います。

## イメージの検索

最新の Ubuntu LTS のスラッグを調べます。

```
curl -sS -H "Authorization: Bearer $DO_TOKEN" \
  "https://api.digitalocean.com/v2/images?type=distribution&per_page=200" | python3 -c '
import json, sys
data = json.load(sys.stdin)
if "images" not in data:
    print("Error:", data.get("message", data)); sys.exit(1)
ubuntu = [i for i in data["images"] if i["distribution"] == "Ubuntu" and "LTS" in i.get("description", "")]
ubuntu.sort(key=lambda x: x["slug"], reverse=True)
for i in ubuntu[:5]:
    slug, desc = i["slug"], i["description"]
    print(f"{slug:30s} {desc}")
'
```

## リージョンの検索

```
curl -sS -H "Authorization: Bearer $DO_TOKEN" \
  "https://api.digitalocean.com/v2/regions" | python3 -c '
import json, sys
data = json.load(sys.stdin)
if "regions" not in data:
    print("Error:", data.get("message", data)); sys.exit(1)
for r in data["regions"]:
    if r["available"]:
        slug, name = r["slug"], r["name"]
        print(f"{slug:10s} {name}")
'
```

## 基本の Droplet 構成

-   リージョン: `nyc3`
-   イメージ: API で利用できる最新の Ubuntu LTS
-   プラン: Basic / Regular (Shared CPU)
-   サイズの例: 2 vCPU / 4 GB RAM / 80 GB SSD / 4 TB 転送量 (月額約 $24)
-   名前: `wlsly-<adjective>-<surname>` (下の「名前ジェネレーター」で生成)

## SSH キー

キーを一覧表示します。

```
curl -sS -H "Authorization: Bearer $DO_TOKEN" \
  "https://api.digitalocean.com/v2/account/keys?per_page=200" | python3 -c '
import json, sys
data = json.load(sys.stdin)
if "ssh_keys" not in data:
    print("Error:", data.get("message", data)); sys.exit(1)
for k in data["ssh_keys"]:
    kid, name, fp = k["id"], k["name"], k["fingerprint"]
    print(f"{kid:>10}  {name:30s}  {fp}")
'
```

新しいキーをアップロードします。

```
curl -sS -X POST \
  -H "Authorization: Bearer $DO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "name": "wlycli", "public_key": "'"$(cat ~/.ssh/wlsly_do.pub)"'" }' \
  "https://api.digitalocean.com/v2/account/keys"
```

## プロジェクト

プロジェクトを一覧表示します。

```
curl -sS -H "Authorization: Bearer $DO_TOKEN" \
  "https://api.digitalocean.com/v2/projects?per_page=200" | python3 -c '
import json, sys
data = json.load(sys.stdin)
if "projects" not in data:
    print("Error:", data.get("message", data)); sys.exit(1)
for p in data["projects"]:
    default = " (default)" if p.get("is_default") else ""
    pid, name = p["id"], p["name"]
    print(f"{pid}  {name}{default}")
'
```

リソースをプロジェクトに割り当てます。

```
curl -sS -X POST \
  -H "Authorization: Bearer $DO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "resources": ["do:droplet:<droplet-id>", "do:dbaas:<db-id>"] }' \
  "https://api.digitalocean.com/v2/projects/<project-id>/resources"
```

## プロジェクトリソースの URN

-   Droplet: `do:droplet:<id>`
-   マネージド DB: `do:dbaas:<id>`

## 名前ジェネレーター

Droplet とデータベース用に、`wlsly-` プレフィックス付きの Docker 風のランダムな名前を生成します。

```
WLSLY_NAME=wlsly-$(shuf -n1 -e agile bold brave bright calm clever cool daring eager fair fast fierce fond frank gifted glad grand happy hardy keen kind lively lucid merry modest noble pensive proud quick quiet sharp shrewd sleek smart solid steady stoic sunny swift tender vivid warm wise witty zen)-$(shuf -n1 -e agnesi babbage bell bohr brahmagupta buck carson chatelet chebyshev curie darwin dijkstra easley einstein euler fermat feynman galois gauss goldberg goodall hamilton hawking hodgkin hopper hypatia jackson johnson kare keller knuth leavitt lovelace lumiere mahavira mandela mccarthy meitner mendel mirzakhani morse nash neumann nightingale noether northcutt panini pare pascal pasteur pike poincare ramanujan ride ritchie rosalind sagan shannon snyder swartz tesla thompson turing villani wescoff wing wozniak wright yalow)
echo "$WLSLY_NAME"
```

生成した名前は、Droplet と (作成する場合は) そのマネージド DB の両方に同じものを使います。

## Droplet の作成

```
curl -sS -X POST \
  -H "Authorization: Bearer $DO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name":"'"$WLSLY_NAME"'",
    "region":"<region>",
    "size":"<size-slug>",
    "image":"<ubuntu-image-slug>",
    "ssh_keys":["<fingerprint>"],
    "monitoring":true,
    "tags":["wlsly"]
  }' \
  "https://api.digitalocean.com/v2/droplets" | python3 -c '
import json, sys
data = json.load(sys.stdin)
if "droplet" not in data:
    print("Error:", data.get("message", data)); sys.exit(1)
d = data["droplet"]
print("Droplet ID:", d["id"])
print("Name:", d["name"])
print("Status:", d["status"])
'
```

## Droplet の IP を確認する

Droplet にはすぐには IP が割り当てられません。`status=active` になるまで繰り返し確認します。

```
curl -sS -H "Authorization: Bearer $DO_TOKEN" \
  "https://api.digitalocean.com/v2/droplets/<droplet-id>" | python3 -c '
import json, sys
data = json.load(sys.stdin)
if "droplet" not in data:
    print("Error:", data.get("message", data)); sys.exit(1)
d = data["droplet"]
print("Status:", d["status"])
for net in d.get("networks", {}).get("v4", []):
    print("IPv4 (%s): %s" % (net["type"], net["ip_address"]))
'
```

## DNS (DigitalOcean で管理)

DNS を DigitalOcean で管理している場合は、Droplet に IP が付いたあと、エージェントが API でレコードを作成・更新できます。

**ドメインがすでに DO にあるか確認します:**

```
curl -sS -H "Authorization: Bearer $DO_TOKEN" \
  "https://api.digitalocean.com/v2/domains/<domain>" | python3 -c '
import json, sys
data = json.load(sys.stdin)
if "domain" in data:
    print("Domain: %s (exists)" % data["domain"]["name"])
else:
    print("Domain not found - needs to be created")
    if "message" in data:
        print("API:", data["message"])
'
```

**ドメインを作成します (見つからなかった場合に、1回だけ):**

```
curl -sS -X POST \
  -H "Authorization: Bearer $DO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"<domain>"}' \
  "https://api.digitalocean.com/v2/domains"
```

**ドメインの既存レコードを一覧表示します:**

```
curl -sS -H "Authorization: Bearer $DO_TOKEN" \
  "https://api.digitalocean.com/v2/domains/<domain>/records?per_page=200" | python3 -c '
import json, sys
data = json.load(sys.stdin)
if "domain_records" not in data:
    print("Error:", data.get("message", data)); sys.exit(1)
for r in data["domain_records"]:
    rid, rtype, name, rdata = r["id"], r["type"], r["name"], r["data"]
    print(f"{rid:>10}  {rtype:6s}  {name:30s}  {rdata}")
'
```

**ルートドメイン (`@`) の A レコードを作成します:**

```
curl -sS -X POST \
  -H "Authorization: Bearer $DO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"type":"A","name":"@","data":"<droplet-ip>","ttl":300}' \
  "https://api.digitalocean.com/v2/domains/<domain>/records"
```

**`storage` サブドメインの A レコードを作成します (CDN がない場合は必須):**

```
curl -sS -X POST \
  -H "Authorization: Bearer $DO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"type":"A","name":"storage","data":"<droplet-ip>","ttl":300}' \
  "https://api.digitalocean.com/v2/domains/<domain>/records"
```

**既存のレコードを更新します (レコードがすでにある場合):**

```
curl -sS -X PUT \
  -H "Authorization: Bearer $DO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"data":"<droplet-ip>"}' \
  "https://api.digitalocean.com/v2/domains/<domain>/records/<record-id>"
```

certbot の standalone を実行する前に、`<domain>` (使う場合は `storage.<domain>` も) の A/AAAA レコードが Droplet の IP を指していることを確認してください。 CLI のフラグ: `--public-ip` (検出された IP を上書き) と `--skip-dns-preflight` (確認をスキップ)。 一時的な環境で自己署名証明書を使う必要がある場合は、`--self-signed-certs` を指定します (自己署名証明書を生成します)。

## DNS に関する注意

CDN を設定していない場合、プラットフォームは `storage.<domain>` を使います。Droplet の IP への A/AAAA レコードを使うか、DNS プロバイダーが対応していればルートドメインへの CNAME を使ってください。

## ホストのプロビジョニング (SSH)

**重要:** 複数行のスクリプトをリモートで実行するときは、必ず `ssh host 'bash -s' << 'EOF'` (クォート付きのヒアドキュメント) を使ってください。スクリプトをダブルクォートの `ssh host "..."` の引数として渡さないでください。リモートホストに送る前に、ローカルのシェル (特に zsh) が `$(...)`、`[...]` のパターン、`$VARIABLES` を展開してしまいます。

```
ssh -o StrictHostKeyChecking=accept-new root@<droplet-ip> 'bash -s' << 'PROVISION'
set -euo pipefail

adduser --disabled-password --gecos "Welley Service" wlsly
usermod -aG sudo wlsly
echo "wlsly ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/wlsly
chmod 440 /etc/sudoers.d/wlsly

mkdir -p /home/wlsly/.ssh
cp /root/.ssh/authorized_keys /home/wlsly/.ssh/authorized_keys
chown -R wlsly:wlsly /home/wlsly/.ssh
chmod 700 /home/wlsly/.ssh
chmod 600 /home/wlsly/.ssh/authorized_keys

apt-get update -qq
apt-get install -y -qq ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" > /etc/apt/sources.list.d/docker.list
apt-get update -qq
apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

usermod -aG docker wlsly

echo "=== Verify ==="
docker --version
docker compose version
id wlsly
PROVISION
```

そのあと、root 以外のユーザーで接続します。

```
ssh -o StrictHostKeyChecking=accept-new wlsly@<droplet-ip> 'echo "Connected as $(whoami)"'
```

## マネージド Postgres (DigitalOcean)

認証情報は、作成した_後_にしかわかりません。先に DB を作成してから、接続情報を取得します。

**作成:**

```
curl -sS -X POST \
  -H "Authorization: Bearer $DO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name":"'"$WLSLY_NAME"'",
    "engine":"pg",
    "version":"16",
    "region":"nyc3",
    "size":"db-s-1vcpu-2gb",
    "num_nodes":1
  }' \
  "https://api.digitalocean.com/v2/databases" | python3 -c '
import json, sys
data = json.load(sys.stdin)
if "database" not in data:
    print("Error:", data.get("message", data)); sys.exit(1)
db = data["database"]
print("DB ID:", db["id"])
print("Status:", db["status"])
'
```

**オンラインになるまで繰り返し確認** (3〜5分かかります):

```
curl -sS -H "Authorization: Bearer $DO_TOKEN" \
  "https://api.digitalocean.com/v2/databases/<db-id>" | python3 -c '
import json, sys
data = json.load(sys.stdin)
if "database" not in data:
    print("Error:", data.get("message", data)); sys.exit(1)
db = data["database"]
print("Status:", db["status"])
if db["status"] == "online":
    conn = db.get("connection", {})
    priv = db.get("private_connection", {})
    print("Host:", conn.get("host"))
    print("Private host:", priv.get("host"))
    print("Port:", conn.get("port"))
    print("Database:", conn.get("database"))
    print("User:", conn.get("user"))
'
```

**DB ユーザーのパスワードを取得** (パスワードはこのエンドポイントからしか取得できません):

```
curl -sS -H "Authorization: Bearer $DO_TOKEN" \
  "https://api.digitalocean.com/v2/databases/<db-id>/users" | python3 -c '
import json, sys
data = json.load(sys.stdin)
if "users" not in data:
    print("Error:", data.get("message", data)); sys.exit(1)
for u in data["users"]:
    print("User:", u["name"], "Password:", u.get("password", "N/A"), "Role:", u.get("role", "N/A"))
'
```

返されたホスト、ポート、データベース、ユーザー、パスワードを、次のオプションに渡します。 `--postgres-host`、`--postgres-port`、`--postgres-database`、`--postgres-user`、`--postgres-password`

パスワードがない、または期限切れの場合は、DB ユーザーの認証をリセットしてください。

## Welley CLI でのデプロイ (例)

シークレットを使った基本の例:

```
wlycli deployment create \
  --domain=example.com \
  --email=admin@example.com \
  --update-period=15min \
  --env=stage \
  --otel-endpoint=https://otlp-gateway-prod-us-east-2.grafana.net/otlp \
  --otel-use-grpc=false \
  --otel-headers='<OTEL_AUTH_HEADERS>' \
  --smtp-host=email-smtp.us-east-2.amazonaws.com \
  --smtp-user=<MAIL_USER> \
  --smtp-password=<MAIL_PASSWORD> \
  --smtp-from=noreply@mail.example.com \
  --language-service=internal \
  --translation-service=deepl \
  --translation-service-api-key=<DEEPL_KEY> \
  --with-sentry
```

外部の Postgres を使う例:

```
wlycli deployment create \
  --domain=example.com \
  --email=admin@example.com \
  --postgres-host=<DB_HOST> \
  --postgres-port=<DB_PORT> \
  --postgres-user=<DB_USER> \
  --postgres-database=<DB_NAME> \
  --postgres-password=<DB_PASSWORD>
```

certbot を実行する前に、DNS の A/AAAA レコードが `<domain>` (使う場合は `storage.<domain>` も) を Droplet の IP に向けていることを確認してください。

## ほかの文書

-   [**Welley CLI** docs/index.md](https://welley.com/ja/administrators/docs/cli/)
-   [**クイックスタート: 最小構成の Welley デプロイ** docs/quick-start.md](https://welley.com/ja/administrators/docs/quick-start/)
-   [**サーバーのデプロイ** skills/deploy-server/SKILL.md](https://welley.com/ja/administrators/docs/deploy-server-skill/)
-   [**openai.yaml** skills/deploy-server/agents/openai.yaml](https://welley.com/ja/administrators/docs/deploy-server-openai-yaml/)
