DigitalOcean
elements man deploy/digitaloceanThe same flow as AWS (opens in a new tab) 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.
Terminalbrew 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.
Terminaldoctl 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
TerminalAPP=$(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 (opens in a new tab). The firewall is a cloud firewall on the tag, 22, 80 and 443 inbound:
Terminaldoctl 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)
TerminalDB=$(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
config.jsocdeploy: { ssh: { user: "root", identityFile: "~/.ssh/elements-<project-id>" }, env: { production: { machines: [ { name: "production0", publicIp: "<public ip>", privateIp: "<private ip>" }, ], }, }, }
config/env/production.envDB_HOST=<host> DB_PORT=<port> DB_USER=<user> DB_PASSWORD=<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 (opens in a new tab)).
Terminalelements deploy -json
No errors in the report is a live site. Nothing is verified afterwards; see Machines (opens in a new tab).
Teardown
Everything carries the elements-<project-id> tag: delete the droplet, the
firewall, the database, then the key. Confirm with the user first.