DigitalOcean Terraform provider
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 syntax
python3 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 exist
python3 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
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 spacesaccessid / spacessecretkey (or SPACESACCESSKEYID / SPACESSECRETACCESSKEY), Spaces resources fail while every other resource applies normally.
Other provider arguments worth knowing: requestspersecond (throttle to avoid rate limits on large applies), httpretrymax, apiendpoint, spacesendpoint. 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 / assignment resource. Using both makes every plan show changes forever. Setting volume_ids also means Terraform claims the Droplet's entire volume set.
- Floating IPs are now reserved IPs.
digitaloceanfloatingip* is deprecated;
use digitaloceanreservedip*.
ssh_keys cannot 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
*_config resources 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_id can never converge — it is a drift bug, not a style nit.
digitaloceancertificate.id is the certificate's name, not its UUID, so the field permanently disagrees with the API. Attach by certificatename, and rotate with a new name plus createbeforedestroy.
- Set
disableletsencryptdnsrecords = true when 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_destroy on a Spaces bucket defaults to false, 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
(digitaloceandroplets, digitaloceanimages, …) for lists.
- **Renaming a volume's
filesystemtype to initialfilesystem_type destroys the
volume and its data.** The replacement is ForceNew and never read back, so on an existing volume it always diffs null → "ext4" and forces replacement. Delete the deprecated argument instead of renaming it (gotchas.md §12b).
Working approach
Writing new configuration
- Find the resource —
dodocs.py list <term> or references/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.md for the
composed shape before assembling it yourself.
- Scan
references/gotchas.md for 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, then terraform validate, then terraform plan.
Reviewing or debugging existing configuration
- Plan never converges / always shows changes → check three things, in this order:
the attach-once rule (gotchas.md §1), certificateid on a load balancer or CDN (§8 — the field is structurally incapable of converging), and *config resources (§5). A config can have more than one cause at once, so keep looking after the first hit.
- Cleaning up deprecated arguments on a live resource → check gotchas.md §12b
first. The obvious fix for a volume's filesystem_type destroys the volume. Read the plan for must be replaced before applying, and stop on a -/+ against anything holding data.
- Unexpected destroy/recreate → look for
ssh_keys edits, 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;
privatenetworking, algorithm, certificateid, and app routes/cors are 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.