# DigitalOcean The same flow as [AWS](aws) on DigitalOcean: sign-in, two questions, the price from the provider, provision, config, deploy. The one difference is sign-in. DigitalOcean has no browser login for its CLI: the user creates an API token in the control panel and pastes it once. The agent opens the page and asks for the paste, and that is the only technical thing the user does. ## 1. CLI and Token `doctl` is one binary. It installs without admin rights. ```bash Terminal brew install doctl 2>/dev/null || { mkdir -p ~/.local/bin curl -fsSL https://github.com/digitalocean/doctl/releases/latest/download/doctl-$(uname -s | tr A-Z a-z)-$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz | tar -xz -C ~/.local/bin } doctl auth init --context elements # paste the token from https://cloud.digitalocean.com/account/api/tokens doctl account get ``` ## 2. Confirm the Shape The same question as on AWS: prototype or production, small or large, region, address. The shapes: | | Small | Large | |---|---|---| | Droplet | `s-1vcpu-1gb` | `s-2vcpu-4gb` | | Database (production) | `db-s-1vcpu-1gb`, one node | `db-s-1vcpu-2gb`, two nodes | ## 3. Get the Price and Confirm Droplet sizes come with prices from the CLI. Database prices do not; open `https://www.digitalocean.com/pricing/managed-databases` in the browser for that line. No price from memory. ```bash Terminal doctl compute size list --format Slug,Memory,VCPUs,Disk,PriceMonthly | grep -E '^(s-1vcpu-1gb|s-2vcpu-4gb) ' ``` Then the monthly figure and "Continue?". Droplets are capped at the monthly price and include a transfer allowance, so the estimate is the bill. ## 4. Key and Machine ```bash Terminal APP=$(cat .elements/id) ssh-keygen -t ed25519 -N "" -f ~/.ssh/elements-$APP -C "elements-$APP" KEY=$(doctl compute ssh-key import "elements-$APP" --public-key-file ~/.ssh/elements-$APP.pub --format ID --no-header) doctl compute droplet create "$APP-production0" --region nyc3 --size s-1vcpu-1gb \ --image ubuntu-24-04-x64 --ssh-keys $KEY --tag-names "elements-$APP,elements-production" \ --wait --format ID,PublicIPv4,PrivateIPv4 ``` Backups stay off: nothing on the droplet is worth backing up, see [Machines](machines). The firewall is a cloud firewall on the tag, 22, 80 and 443 inbound: ```bash Terminal doctl compute firewall create --name "elements-$APP" --tag-names "elements-$APP" \ --inbound-rules "protocol:tcp,ports:22,address:0.0.0.0/0,address:::/0 protocol:tcp,ports:80,address:0.0.0.0/0,address:::/0 protocol:tcp,ports:443,address:0.0.0.0/0,address:::/0" \ --outbound-rules "protocol:tcp,ports:all,address:0.0.0.0/0,address:::/0 protocol:udp,ports:all,address:0.0.0.0/0,address:::/0" ``` ## 5. The Database (Production) ```bash Terminal DB=$(doctl databases create "elements-$APP" --engine pg --version 17 --region nyc3 \ --size db-s-1vcpu-1gb --num-nodes 1 --wait --format ID --no-header) doctl databases firewalls append $DB --rule tag:elements-$APP doctl databases connection $DB --format Host,Port,User,Password mkdir -p config/certs && doctl databases get-ca $DB > config/certs/db-ca.pem ``` Large is `db-s-1vcpu-2gb` with `--num-nodes 2`. Trusted sources on the database are the droplet's tag, so only the app machine reaches it. Daily backups with seven days of retention are included. ## 6. Config and the First Deploy ```jsoc config.jsoc deploy: { ssh: { user: "root", identityFile: "~/.ssh/elements-" }, env: { production: { machines: [ { name: "production0", publicIp: "", privateIp: "" }, ], }, }, } ``` ```text config/env/production.env DB_HOST= DB_PORT= DB_USER= DB_PASSWORD= DB_SSL_ROOT_CERT=config/certs/db-ca.pem ``` A prototype keeps `DB_HOST=127.0.0.1`. The domain is as on AWS: their own in `deploy.domain` with an A record at the droplet's address, or `elements domain` for an elements.online name (see [Domain](domain)). ```bash Terminal elements deploy -json ``` No errors in the report is a live site. Nothing is verified afterwards; see [Machines](machines). ## Teardown Everything carries the `elements-` tag: delete the droplet, the firewall, the database, then the key. Confirm with the user first.