Referencia de DigitalOcean
Todos los fragmentos usan curl + python3 (sin dependencias externas). Define DO_TOKEN antes de ejecutarlos.
Compatibilidad con el shell: Todos los argumentos de python3 -c usan comillas simples, así que ni bash ni zsh expanden variables. El código Python usa solo comillas dobles.
Cómo definir DO_TOKEN: En zsh, DO_TOKEN=... curl ... en una sola línea NO funciona: $DO_TOKEN se expande antes de que la asignación surta efecto. Usa una de estas opciones:
export DO_TOKEN=... # then run snippets normally
DO_TOKEN=...; curl ... # semicolon separates assignment from command
Manejo de errores: Todos los fragmentos comprueban si hay errores de la API de DO antes de acceder a los datos. Los errores de DO tienen el formato {"id": "...", "message": "..."}.
Tamaños de instancia
Lista y filtra los tamaños disponibles:
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")
'
Ajusta region, vcpus y mem_gb según lo necesites. Usa >= en lugar de == para buscar tamaños que cumplan como mínimo esos valores.
Descubrir imágenes
Obtén el slug de la última versión LTS de Ubuntu:
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}")
'
Descubrir regiones
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}")
'
Perfil base del droplet
- Región:
nyc3 - Imagen: la última versión LTS de Ubuntu disponible en la API
- Plan: Basic / Regular (Shared CPU)
- Tamaño de ejemplo: 2 vCPU / 4 GB de RAM / 80 GB de SSD / 4 TB de transferencia (~$24/mes)
- Nombre:
wlsly-<adjective>-<surname>(generado con el generador de nombres que aparece más abajo)
Claves SSH
Lista las claves:
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}")
'
Sube una clave nueva:
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"
Proyectos
Lista los proyectos:
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}")
'
Asigna recursos a un proyecto:
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 de los recursos del proyecto
- Droplet:
do:droplet:<id> - Base de datos administrada:
do:dbaas:<id>
Generador de nombres
Genera un nombre aleatorio al estilo de Docker, con el prefijo wlsly-, para droplets y bases de datos:
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"
Usa el mismo nombre generado para el droplet y para su base de datos administrada (si la creas).
Creación del 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"])
'
Consultar la IP del droplet
El droplet no tendrá una IP de inmediato. Consulta su estado hasta que aparezca 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 (administrado por DigitalOcean)
Si DigitalOcean administra el DNS, el agente puede crear o actualizar registros mediante la API cuando el droplet ya tenga una IP.
Comprueba si el dominio ya existe en 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"])
'
Crea un dominio (solo una vez, si no existe):
curl -sS -X POST \
-H "Authorization: Bearer $DO_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"<domain>"}' \
"https://api.digitalocean.com/v2/domains"
Lista los registros existentes de un dominio:
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}")
'
Crea un registro A para el dominio raíz (@):
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"
Crea un registro A para el subdominio storage (obligatorio cuando no hay 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"
Actualiza un registro existente (si el registro ya existe):
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>"
Antes de ejecutar certbot en modo standalone, asegúrate de que los registros A/AAAA de <domain> (y de storage.<domain> si lo usas) apunten a la IP del droplet.
Opciones de CLI: --public-ip (reemplaza la IP detectada) y --skip-dns-preflight (omite la comprobación).
Si necesitas usar certificados autofirmados en una configuración temporal, pasa --self-signed-certs (genera certificados autofirmados).
Notas sobre DNS
Si no hay un CDN configurado, la plataforma usa storage.<domain>. Usa registros A/AAAA hacia la IP del droplet, o un CNAME hacia el dominio raíz si tu proveedor de DNS lo permite.
Aprovisionamiento del host (SSH)
Importante: Usa siempre ssh host 'bash -s' << 'EOF' (heredoc entre comillas) para ejecutar scripts de varias líneas de forma remota. Nunca pases los scripts como argumentos ssh host "..." entre comillas dobles: el shell local (sobre todo zsh) expandirá $(...), los patrones [...] y $VARIABLES antes de enviarlos al host remoto.
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
Luego cambia al usuario que no es root:
ssh -o StrictHostKeyChecking=accept-new wlsly@<droplet-ip> 'echo "Connected as $(whoami)"'
Postgres administrado (DigitalOcean)
Las credenciales solo se conocen después de crear la base de datos. Primero crea la base de datos y luego lee los datos de conexión.
Crear:
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"])
'
Consulta hasta que esté en línea (tarda de 3 a 5 minutos):
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"))
'
Obtén la contraseña del usuario de la base de datos (la contraseña solo está disponible en este endpoint):
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"))
'
Usa el host, el puerto, la base de datos, el usuario y la contraseña que obtuviste con estas opciones:
--postgres-host, --postgres-port, --postgres-database, --postgres-user, --postgres-password.
Si falta la contraseña o ya venció, restablece la autenticación del usuario de la base de datos.
Despliegue con Welley CLI (ejemplos)
Básico, con secretos:
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 externo:
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>
Antes de ejecutar certbot, asegúrate de que los registros DNS A/AAAA de <domain> (y de storage.<domain> si lo usas) apunten a la IP del droplet.