skills/deploy-server/references/digitalocean.md

# DigitalOcean-viite

Kaikki koodinpätkät käyttävät työkaluja `curl` + `python3` (ei ulkoisia riippuvuuksia). Aseta `DO_TOKEN` ennen ajamista.

**Yhteensopivuus shellien kanssa:** Kaikissa `python3 -c` -argumenteissa on yksinkertaiset lainausmerkit, jotta bash tai zsh ei laajenna muuttujia. Python-koodissa käytetään vain kaksinkertaisia lainausmerkkejä.

**DO\_TOKENin asettaminen:** zsh:ssa `DO_TOKEN=... curl ...` yhdellä rivillä EI toimi: `$DO_TOKEN` laajennetaan ennen kuin sijoitus tulee voimaan. Käytä jompaakumpaa:

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

**Virheiden käsittely:** Kaikki koodinpätkät tarkistavat DO API:n virheet ennen tietojen käyttöä. DO:n virheillä on muoto `{"id": "...", "message": "..."}`.

## Instanssikoot

Listaa ja suodata saatavilla olevat koot:

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

Muuta arvoja `region`, `vcpus` ja `mem_gb` tarpeen mukaan. Käytä merkintää `>=` merkinnän `==` sijaan, jos haluat hakea vähimmäisvaatimuksen mukaan.

## Käyttöjärjestelmäkuvien haku

Hae uusimman Ubuntu LTS:n slug-tunniste:

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

## Alueiden haku

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

## Dropletin perusprofiili

-   Alue: `nyc3`
-   Käyttöjärjestelmäkuva: uusin API:n kautta saatavilla oleva Ubuntu LTS
-   Paketti: Basic / Regular (Shared CPU)
-   Esimerkkikoko: 2 vCPU / 4 GB RAM / 80 GB SSD / 4 TB siirtoa (~$24/kk)
-   Nimi: `wlsly-<adjective>-<surname>` (luodaan alla olevalla nimigeneraattorilla)

## SSH-avaimet

Listaa avaimet:

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

Lisää uusi avain:

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

## Projektit

Listaa projektit:

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

Liitä resursseja projektiin:

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

## Projektin resurssien URN-tunnisteet

-   Droplet: `do:droplet:<id>`
-   Hallinnoitu tietokanta: `do:dbaas:<id>`

## Nimigeneraattori

Luo dropleteille ja tietokannoille satunnainen Docker-tyylinen nimi, jossa on etuliite `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"
```

Käytä samaa luotua nimeä sekä dropletille että sen hallinnoidulle tietokannalle (jos sellainen luodaan).

## Dropletin luonti

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

## Dropletin IP-osoitteen kysely

Dropletilla ei ole IP-osoitetta heti. Kysele, kunnes `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 (DigitalOceanin hallinnoima)

Jos DNS:ää hallinnoi DigitalOcean, agentti voi luoda ja päivittää tietueita API:n kautta, kun dropletille on tullut IP-osoite.

**Tarkista, onko verkkotunnus jo DO:ssa:**

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

**Luo verkkotunnus (vain kerran, jos sitä ei löydy):**

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

**Listaa verkkotunnuksen nykyiset tietueet:**

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

**Luo A-tietue juuriverkkotunnukselle (`@`):**

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

**Luo A-tietue `storage`\-aliverkkotunnukselle (pakollinen, kun CDN:ää ei ole):**

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

**Päivitä olemassa oleva tietue (jos tietue on jo olemassa):**

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

Varmista ennen certbotin ajamista standalone-tilassa, että verkkotunnuksen `<domain>` (ja `storage.<domain>`, jos käytössä) A/AAAA-tietueet osoittavat dropletin IP-osoitteeseen. CLI-liput: `--public-ip` (korvaa tunnistetun IP-osoitteen) ja `--skip-dns-preflight` (ohittaa tarkistuksen). Jos tilapäisessä asennuksessa on pakko käyttää itse allekirjoitettuja varmenteita, käytä lippua `--self-signed-certs` (luo itse allekirjoitetut varmenteet).

## DNS-huomioita

Jos CDN:ää ei ole määritetty, alusta käyttää osoitetta `storage.<domain>`. Käytä A/AAAA-tietueita dropletin IP-osoitteeseen tai CNAME-tietuetta juuriverkkotunnukseen, jos DNS-palveluntarjoajasi sallii sen.

## Palvelimen valmistelu (SSH)

**Tärkeää:** Käytä monirivisten skriptien ajamiseen etäkoneella aina muotoa `ssh host 'bash -s' << 'EOF'` (lainausmerkeillä suojattu heredoc). Älä koskaan välitä skriptejä kaksinkertaisilla lainausmerkeillä kirjoitettuina `ssh host "..."` -argumentteina: paikallinen shell (varsinkin zsh) laajentaa `$(...)`\- ja `[...]`\-kuviot sekä `$VARIABLES`\-muuttujat ennen kuin ne lähetetään etäkoneelle.

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

Vaihda sitten käyttäjään, jolla ei ole root-oikeuksia:

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

## Hallinnoitu Postgres (DigitalOcean)

Tunnukset tiedetään vasta _luomisen jälkeen_. Luo ensin tietokanta ja lue sitten yhteystiedot.

**Luo:**

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

**Kysele, kunnes tila on online** (kestää 3-5 minuuttia):

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

**Hae tietokantakäyttäjän salasana** (salasana on saatavilla vain tästä päätepisteestä):

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

Anna palautetut arvot (isäntä, portti, tietokanta, käyttäjä ja salasana) näillä lipuilla: `--postgres-host`, `--postgres-port`, `--postgres-database`, `--postgres-user`, `--postgres-password`.

Jos salasana puuttuu tai on vanhentunut, nollaa tietokantakäyttäjän todennus.

## Welley CLI -käyttöönotto (esimerkkejä)

Perusesimerkki salaisuuksilla:

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

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

Varmista ennen certbotin ajamista, että DNS:n A/AAAA-tietueet osoittavat verkkotunnuksen `<domain>` (ja `storage.<domain>`, jos käytössä) dropletin IP-osoitteeseen.

## Muut asiakirjat

-   [**Welley CLI** docs/index.md](https://welley.com/fi/administrators/docs/cli/)
-   [**Pikaopas: Welleyn minimiasennus** docs/quick-start.md](https://welley.com/fi/administrators/docs/quick-start/)
-   [**Palvelimen käyttöönotto** skills/deploy-server/SKILL.md](https://welley.com/fi/administrators/docs/deploy-server-skill/)
-   [**openai.yaml** skills/deploy-server/agents/openai.yaml](https://welley.com/fi/administrators/docs/deploy-server-openai-yaml/)
