Everything here is derived from the official digitalocean/digitalocean provider
docs at v2.99.1, bundled verbatim under references/provider-docs/. Those files
are authoritative on argument names, types, defaults, and import syntax — prefer them
over recollection, because this provider changes often and several of its resources
have near-identical siblings that take different arguments.
Start here
Never write digitalocean_* HCL from memory. The failure mode is not a clear
error — it is configuration that looks correct, passes review, and then either fails
at apply or produces a plan that never converges. Confirm arguments first; it costs
one command.
# What exists? (fuzzy, matches resources and data sources)
python3 scripts/dodocs.py list database
Exact arguments and attributes for one resource
python3 scripts/dodocs.py args droplet
Just the example, or just the import syntaxpython3 scripts/dodocs.py example loadbalancer python3 scripts/dodocs.py import database_db
Which resources mention an argument at all?python3 scripts/dodocs.py search vpc_uuid
Data source rather than resource, when both names existpython3 scripts/dodocs.py args kubernetes_cluster --kind data
Run from the skill directory. Names work with or without the `digitalocean_` prefix,
and a wrong guess suggests near matches instead of failing silently.
## The files
| File | Read it when |
|---|---|
| `references/gotchas.md` | **Before writing or reviewing any config.** Cross-cutting traps: perpetual drift, forced replacement, deprecated arguments, Spaces credentials. Short, and prevents most real-world breakage. |
| `references/patterns.md` | You need more than one resource wired together — web tier, database + firewall, DOKS, Spaces + CDN, App Platform, projects, imports. |
| `references/resource-index.md` | You know the goal but not the resource name. All 131 entries with one-line purposes, grouped by subcategory. |
| `references/deprecations.md` | Auditing an inherited config, or a plan won't converge. All nine deprecated arguments quoted from the provider's compiled schema — these messages are what `terraform validate` prints, and several are more specific than the markdown docs. |
| `references/provider-docs/` | You need exact arguments. Upstream docs verbatim: `resources/<name>.md`, `data-sources/<name>.md`, plus `index.md` for provider configuration. |
## Provider setup
```hcl
terraform {
required_providers {
digitalocean = {
source = "digitalocean/digitalocean"
version = "~> 2.99"
}
}
}
provider "digitalocean" {
token = var.do_token # or the DIGITALOCEAN_TOKEN environment variable
}
The token grants full account access, so keep it in an environment variable or a secret manager — never committed, and never a default value on a variable.
Spaces is the exception that catches everyone. Spaces resources authenticate with
S3-style keys, not the API token. Without spaces_access_id / spaces_secret_key
(or SPACES_ACCESS_KEY_ID / SPACES_SECRET_ACCESS_KEY), Spaces resources fail while
every other resource applies normally.
Other provider arguments worth knowing: requests_per_second (throttle to avoid rate
limits on large applies), http_retry_max, api_endpoint, spaces_endpoint. Full
list in references/provider-docs/index.md.
Where things live
The provider's own subcategories, with what each covers:
| Area | Covers |
|---|---|
| Networking (33) | VPC, peering, NAT gateway, Cloud Firewall, Load Balancer, certificates, reserved IP/IPv6 (+assignments), DNS domains and records, CDN, BYOIP, Partner Network Connect |
| Databases (28) | Managed clusters, users, DBs, connection pools, DB firewalls, replicas, online migration, log sinks, and per-engine config resources (mysql, postgresql, valkey, redis, mongodb, kafka, opensearch) |
| Account (13) | Projects and project resources, tags, SSH keys, account/region/size lookups |
| Spaces Object Storage (12) | Buckets, objects, policies, CORS, logging, access keys |
| Droplets (8) | Droplets, autoscale pools, custom images, image lookups |
| Dedicated Inference (8) | Dedicated inference endpoints, tokens, GPU model config, sizes |
| NFS Storage (7) | NFS shares, access points, snapshots, attachments |
| Backups & Snapshots (5) | Droplet and volume snapshots |
| Kubernetes (4) | DOKS clusters, node pools, version lookups |
| Volumes Block Storage (3) | Volumes and volume attachments |
| Monitoring (3) | Metric alerts, uptime checks, uptime alerts |
| Container Registry (3) | Registry and Docker credentials |
| GradientAI (2) | GradientAI agents and knowledge bases |
| App Platform (2) | digitalocean_app and its data source |
Full per-resource breakdown with purposes: references/resource-index.md.
The traps that actually bite
These cause the majority of broken DigitalOcean configurations. Details and fixes in
references/gotchas.md.
- Attach exactly one way. Volumes, reserved IPs, reserved IPv6s, and floating
IPs can each be attached inline on the Droplet or through a separate
*_attachment/*_assignmentresource. Using both makes every plan show changes forever. Settingvolume_idsalso means Terraform claims the Droplet's entire volume set. - Floating IPs are now reserved IPs.
digitalocean_floating_ip*is deprecated; usedigitalocean_reserved_ip*. ssh_keyscannot change after creation. It takes key IDs or fingerprints, not key material, and editing it destroys and recreates the Droplet.- Don't create a DOKS cluster and its Kubernetes resources in one module. Terraform evaluates provider blocks before resources exist. Split into two applies.
- Database
*_configresources don't unset remote settings on destroy. They only leave state. Reset values explicitly first. - Managed Redis was discontinued (30 June 2025). Use Valkey for anything new.
certificate_idcan never converge — it is a drift bug, not a style nit.digitalocean_certificate.idis the certificate's name, not its UUID, so the field permanently disagrees with the API. Attach bycertificate_name, and rotate with a new name pluscreate_before_destroy.- Set
disable_lets_encrypt_dns_records = truewhen a load balancer has a Let's Encrypt cert and you also manage that hostname's DNS record in Terraform — otherwise DigitalOcean's auto-created record fights yours. force_destroyon a Spaces bucket defaults tofalse, so destroy fails on a non-empty bucket.- Projects take URNs (
.urn), not IDs. - Singular data sources error on zero or multiple matches. Use the plural forms
(
digitalocean_droplets,digitalocean_images, …) for lists.
Working approach
Writing new configuration
- Find the resource —
dodocs.py list <term>orreferences/resource-index.md. - Read its real arguments —
dodocs.py args <name>. Note which are(Required)and which say "Changing this forces a new resource". - If more than one resource is involved, check
references/patterns.mdfor the composed shape before assembling it yourself. - Scan
references/gotchas.mdfor anything touching the resources you used. - Look up regions, sizes, and versions with data sources rather than hardcoding slugs — availability changes per account and region.
- Verify:
terraform fmt, thenterraform validate, thenterraform plan.
Reviewing or debugging existing configuration
- Plan never converges / always shows changes → check three things, in this order:
the attach-once rule (§1),
certificate_idon a load balancer or CDN (§7 — the field is structurally incapable of converging), and*_configresources. A config can have more than one cause at once, so keep looking after the first hit. - Unexpected destroy/recreate → look for
ssh_keysedits, or an argument the docs mark as forcing a new resource.dodocs.py args <name>shows these. - Auth failures on only some resources → missing Spaces credentials.
- Copied from a blog post → check the deprecated-argument table in gotchas.md §9;
private_networking,algorithm,certificate_id, and approutes/corsare the usual survivors.
Verification honesty: terraform validate checks syntax and types but does not
contact the API, so it cannot confirm that a region supports a size or that a slug
exists. Only terraform plan (with credentials) does. Say which one you actually
ran rather than implying more coverage than you have.
Version note
Bundled docs are v2.99.1 (published 2026-08-06). If the user pins a different major
or a much newer minor version, arguments may differ — check the upstream repo at
github.com/digitalocean/terraform-provider-digitalocean under docs/, or the
registry, and say so rather than assuming these docs still apply.