Справочник по DigitalOcean
Во всех примерах используются curl + python3 (без внешних зависимостей). Перед запуском задайте DO_TOKEN.
Совместимость с оболочками: во всех аргументах python3 -c используются одинарные кавычки, поэтому ни bash, ни zsh не подставляют переменные. В коде Python используются только двойные кавычки.
Как задать DO_TOKEN: в zsh запись DO_TOKEN=... curl ... в одной строке НЕ работает: $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 под себя. Чтобы искать по минимальному значению, используйте >= вместо ==.
Поиск образа
Узнайте slug последней версии 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}")
'
Базовый профиль дроплета
- Регион:
nyc3 - Образ: последняя версия Ubuntu LTS, доступная через API
- Тариф: 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 ресурсов проекта
- Дроплет:
do:droplet:<id> - Управляемая база данных:
do:dbaas:<id>
Генератор имён
Создайте случайное имя в стиле Docker с префиксом wlsly- для дроплетов и баз данных:
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"
Используйте одно и то же имя для дроплета и его управляемой базы данных (если вы её создаёте).
Создание дроплета
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"])
'
Ожидание IP-адреса дроплета
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, агент может создать или обновить записи через API, когда у дроплета появится IP-адрес.
Проверьте, есть ли уже домен в 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"])
'
Создайте домен (один раз, если он не найден):
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"
Создайте A-запись для поддомена storage (нужна, если 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 убедитесь, что A/AAAA-записи для <domain> (и storage.<domain>, если он используется) указывают на IP-адрес дроплета.
Флаги CLI: --public-ip (переопределяет определённый IP-адрес) и --skip-dns-preflight (пропускает проверку).
Если для временной настройки нужны самоподписанные сертификаты, передайте --self-signed-certs (создаёт самоподписанные сертификаты).
Заметки о DNS
Если CDN не настроен, платформа использует storage.<domain>. Создайте A/AAAA-записи с IP-адресом дроплета или CNAME-запись на корневой домен, если ваш DNS-провайдер это позволяет.
Настройка сервера (SSH)
Важно: всегда используйте ssh host 'bash -s' << 'EOF' (heredoc в кавычках), чтобы запускать многострочные скрипты на удалённом сервере. Никогда не передавайте скрипты в аргументах 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)
Учётные данные становятся известны только после создания. Сначала создайте базу, затем прочитайте данные для подключения.
Создание:
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"])
'
Дождитесь статуса online (это занимает 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"))
'
Получите пароль пользователя базы (пароль доступен только через этот эндпоинт):
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"))
'
Используйте полученные host, port, database, user и password со следующими флагами:
--postgres-host, --postgres-port, --postgres-database, --postgres-user, --postgres-password.
Если пароля нет или он истёк, сбросьте аутентификацию пользователя базы.
Развёртывание через 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 убедитесь, что A/AAAA-записи DNS для <domain> (и storage.<domain>, если он используется) указывают на IP-адрес дроплета.