DigitalOcean-Referenz
Alle Snippets verwenden curl + python3 (keine externen Abhängigkeiten). Setze DO_TOKEN, bevor du sie ausführst.
Shell-Kompatibilität: Alle python3 -c-Argumente stehen in einfachen Anführungszeichen, damit weder bash noch zsh Variablen ersetzt. Der Python-Code verwendet nur doppelte Anführungszeichen.
DO_TOKEN setzen: In zsh funktioniert DO_TOKEN=... curl ... in einer Zeile NICHT: $DO_TOKEN wird ersetzt, bevor die Zuweisung greift. Nimm eine dieser Varianten:
export DO_TOKEN=... # then run snippets normally
DO_TOKEN=...; curl ... # semicolon separates assignment from command
Fehlerbehandlung: Alle Snippets prüfen auf DO-API-Fehler, bevor sie auf Daten zugreifen. DO-Fehler haben das Format {"id": "...", "message": "..."}.
Instanzgrößen
Verfügbare Größen auflisten und filtern:
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")
'
Passe region, vcpus und mem_gb nach Bedarf an. Verwende >= statt ==, wenn es um einen Mindestwert gehen soll.
Images finden
Den Slug der neuesten Ubuntu LTS ermitteln:
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}")
'
Regionen finden
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}")
'
Standardprofil für Droplets
- Region:
nyc3 - Image: neueste Ubuntu LTS, die über die API verfügbar ist
- Plan: Basic / Regular (Shared CPU)
- Beispielgröße: 2 vCPU / 4 GB RAM / 80 GB SSD / 4 TB Transfer (ca. 24 $ pro Monat)
- Name:
wlsly-<adjective>-<surname>(erzeugt mit dem Namensgenerator weiter unten)
SSH-Schlüssel
Schlüssel auflisten:
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}")
'
Neuen Schlüssel hochladen:
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"
Projekte
Projekte auflisten:
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}")
'
Ressourcen einem Projekt zuweisen:
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"
URNs für Projektressourcen
- Droplet:
do:droplet:<id> - Verwaltete DB:
do:dbaas:<id>
Namensgenerator
Erzeuge einen zufälligen Namen im Docker-Stil mit dem Präfix wlsly- für Droplets und Datenbanken:
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"
Verwende denselben erzeugten Namen für das Droplet und seine verwaltete DB (falls du eine anlegst).
Droplet erstellen
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 nach der IP abfragen
Das Droplet hat nicht sofort eine IP. Frage so lange ab, bis status=active gilt:
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 (von DigitalOcean verwaltet)
Wenn DigitalOcean das DNS verwaltet, kann der Agent Einträge per API anlegen oder aktualisieren, sobald das Droplet eine IP hat.
Prüfen, ob die Domain bereits in DO existiert:
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"])
'
Domain anlegen (nur einmal, falls sie nicht gefunden wurde):
curl -sS -X POST \
-H "Authorization: Bearer $DO_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"<domain>"}' \
"https://api.digitalocean.com/v2/domains"
Vorhandene Einträge einer Domain auflisten:
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-Eintrag für die Root-Domain (@) anlegen:
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-Eintrag für die Subdomain storage anlegen (erforderlich, wenn kein CDN genutzt wird):
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"
Bestehenden Eintrag aktualisieren (falls er schon existiert):
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>"
Bevor certbot standalone läuft, stelle sicher, dass die A/AAAA-Einträge für <domain> (und storage.<domain>, falls verwendet) auf die Droplet-IP zeigen.
CLI-Flags: --public-ip (überschreibt die erkannte IP) und --skip-dns-preflight (überspringt die Prüfung).
Wenn du für ein temporäres Setup selbst signierte Zertifikate brauchst, gib --self-signed-certs an (erzeugt selbst signierte Zertifikate).
Hinweise zu DNS
Ist kein CDN konfiguriert, nutzt die Plattform storage.<domain>. Verwende A/AAAA auf die Droplet-IP oder CNAME auf die Root-Domain, wenn dein DNS-Anbieter das erlaubt.
Host einrichten (SSH)
Wichtig: Verwende zum Ausführen mehrzeiliger Skripte auf dem Server immer ssh host 'bash -s' << 'EOF' (Heredoc in Anführungszeichen). Übergib Skripte nie als ssh host "..."-Argumente in doppelten Anführungszeichen: Die lokale Shell (vor allem zsh) ersetzt $(...), [...]-Muster und $VARIABLES, bevor die Skripte an den entfernten Host gesendet werden.
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
Wechsle dann zum Nutzer, der nicht root ist:
ssh -o StrictHostKeyChecking=accept-new wlsly@<droplet-ip> 'echo "Connected as $(whoami)"'
Verwaltetes Postgres (DigitalOcean)
Die Zugangsdaten stehen erst nach dem Anlegen fest. Lege zuerst die DB an und lies dann die Verbindungsdaten aus.
Anlegen:
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"])
'
Abfragen, bis die DB online ist (dauert 3 bis 5 Minuten):
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"))
'
Passwort des DB-Nutzers abrufen (das Passwort gibt es nur über diesen 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"))
'
Verwende die zurückgegebenen Werte für Host, Port, Datenbank, Nutzer und Passwort mit:
--postgres-host, --postgres-port, --postgres-database, --postgres-user, --postgres-password.
Fehlt das Passwort oder ist es abgelaufen, setze die Authentifizierung des DB-Nutzers zurück.
Deployment mit der Welley CLI (Beispiele)
Einfaches Beispiel mit Secrets:
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
Externes 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>
Bevor du certbot ausführst, stelle sicher, dass die DNS-A/AAAA-Einträge für <domain> (und storage.<domain>, falls verwendet) auf die Droplet-IP zeigen.