skills/deploy-server/references/digitalocean.md

# Dokumentacja DigitalOcean

Wszystkie fragmenty używają `curl` i `python3` (bez zewnętrznych zależności). Przed uruchomieniem ustaw `DO_TOKEN`.

**Zgodność z powłokami:** Wszystkie argumenty `python3 -c` są w apostrofach, więc ani bash, ani zsh nie rozwija zmiennych. Kod Pythona używa tylko podwójnych cudzysłowów.

**Ustawianie DO\_TOKEN:** W zsh zapis `DO_TOKEN=... curl ...` w jednej linii NIE działa: `$DO_TOKEN` jest rozwijany, zanim przypisanie zacznie obowiązywać. Użyj jednego z tych sposobów:

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

**Obsługa błędów:** Wszystkie fragmenty sprawdzają, czy API DigitalOcean nie zwróciło błędu, zanim sięgną po dane. Błędy DO mają format `{"id": "...", "message": "..."}`.

## Rozmiary instancji

Wyświetl i przefiltruj dostępne rozmiary:

```
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")
'
```

Dostosuj `region`, `vcpus` i `mem_gb` do swoich potrzeb. Użyj `>=` zamiast `==`, żeby dopasować minimum.

## Wyszukiwanie obrazów

Znajdź slug najnowszego 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}")
'
```

## Wyszukiwanie regionów

```
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}")
'
```

## Podstawowy profil dropleta

-   Region: `nyc3`
-   Obraz: najnowsze Ubuntu LTS dostępne przez API
-   Plan: Basic / Regular (Shared CPU)
-   Przykładowy rozmiar: 2 vCPU / 4 GB RAM / 80 GB SSD / 4 TB transferu (ok. 24 USD/mies.)
-   Nazwa: `wlsly-<adjective>-<surname>` (wygenerowana generatorem nazw poniżej)

## Klucze SSH

Wyświetl klucze:

```
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}")
'
```

Wgraj nowy klucz:

```
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"
```

## Projekty

Wyświetl projekty:

```
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}")
'
```

Przypisz zasoby do projektu:

```
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-y zasobów projektu

-   Droplet: `do:droplet:<id>`
-   Zarządzana baza danych: `do:dbaas:<id>`

## Generator nazw

Wygeneruj losową nazwę w stylu Dockera z prefiksem `wlsly-` dla dropletów i baz danych:

```
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"
```

Użyj tej samej wygenerowanej nazwy dla dropleta i jego zarządzanej bazy danych (jeśli ją tworzysz).

## Tworzenie dropleta

```
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"])
'
```

## Sprawdzanie IP dropleta

Droplet nie dostanie adresu IP od razu. Odpytuj go, aż pojawi się `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 (zarządzany przez DigitalOcean)

Jeśli DNS jest zarządzany przez DigitalOcean, agent może tworzyć i aktualizować rekordy przez API, gdy droplet dostanie adres IP.

**Sprawdź, czy domena już istnieje w 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"])
'
```

**Utwórz domenę (tylko raz, jeśli nie została znaleziona):**

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

**Wyświetl istniejące rekordy domeny:**

```
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}")
'
```

**Utwórz rekord A dla domeny głównej (`@`):**

```
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"
```

**Utwórz rekord A dla subdomeny `storage` (wymagany, gdy nie ma 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"
```

**Zaktualizuj istniejący rekord (jeśli rekord już istnieje):**

```
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>"
```

Zanim uruchomisz certbota w trybie standalone, upewnij się, że rekordy A/AAAA dla `<domain>` (oraz `storage.<domain>`, jeśli jest używana) wskazują na IP dropleta. Flagi CLI: `--public-ip` (nadpisuje wykryte IP) i `--skip-dns-preflight` (pomija sprawdzenie). Jeśli na potrzeby tymczasowej konfiguracji musisz użyć certyfikatów z własnym podpisem, dodaj `--self-signed-certs` (generuje certyfikaty z własnym podpisem).

## Uwagi o DNS

Jeśli CDN nie jest skonfigurowany, platforma używa `storage.<domain>`. Ustaw rekordy A/AAAA na IP dropleta albo CNAME na domenę główną, jeśli twój dostawca DNS na to pozwala.

## Przygotowanie hosta (SSH)

**Ważne:** Do zdalnego uruchamiania wieloliniowych skryptów zawsze używaj `ssh host 'bash -s' << 'EOF'` (heredoc w apostrofach). Nigdy nie przekazuj skryptów jako argumentów `ssh host "..."` w podwójnych cudzysłowach: lokalna powłoka (zwłaszcza zsh) rozwinie `$(...)`, wzorce `[...]` i `$VARIABLES`, zanim wyśle je na zdalny host.

```
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
```

Potem przełącz się na użytkownika innego niż root:

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

## Zarządzany Postgres (DigitalOcean)

Dane logowania poznasz dopiero _po_ utworzeniu bazy. Najpierw utwórz bazę, potem odczytaj dane połączenia.

**Utwórz:**

```
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"])
'
```

**Odpytuj, aż baza będzie online** (trwa to 3-5 minut):

```
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"))
'
```

**Pobierz hasło użytkownika bazy** (hasło jest dostępne tylko z tego endpointu):

```
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"))
'
```

Zwrócone wartości host, port, database, user i password podaj w: `--postgres-host`, `--postgres-port`, `--postgres-database`, `--postgres-user`, `--postgres-password`.

Jeśli hasła brakuje albo wygasło, zresetuj uwierzytelnianie użytkownika bazy.

## Wdrażanie przez Welley CLI (przykłady)

Podstawowe wdrożenie z sekretami:

```
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
```

Zewnętrzny 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>
```

Przed uruchomieniem certbota upewnij się, że rekordy DNS A/AAAA kierują `<domain>` (oraz `storage.<domain>`, jeśli jest używana) na IP dropleta.

## Inne dokumenty

-   [**Welley CLI** docs/index.md](https://welley.com/pl/administrators/docs/cli/)
-   [**Szybki start: minimalne wdrożenie Welley** docs/quick-start.md](https://welley.com/pl/administrators/docs/quick-start/)
-   [**Wdrożenie serwera** skills/deploy-server/SKILL.md](https://welley.com/pl/administrators/docs/deploy-server-skill/)
-   [**openai.yaml** skills/deploy-server/agents/openai.yaml](https://welley.com/pl/administrators/docs/deploy-server-openai-yaml/)
