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 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 / *_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.
digitalocean_floating_ip* is deprecated;
use digitalocean_reserved_ip*.
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.
digitalocean_certificate.id is the certificate's name, not its UUID, so the
field permanently disagrees with the API. Attach by certificate_name, and rotate
with a new name plus create_before_destroy.
- Set
disable_lets_encrypt_dns_records = 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
(
digitalocean_droplets, digitalocean_images, …) for lists.
- Renaming a volume's
filesystem_type to initial_filesystem_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),
certificate_id 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;
private_networking, algorithm, certificate_id, 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.
1---2name: digitalocean-terraform3description: Write, review, and debug Terraform/OpenTofu configuration for DigitalOcean using the digitalocean/digitalocean provider. Bundles the complete provider reference (70 resources, 61 data sources) plus cross-cutting gotchas and composed multi-resource stacks. Use this whenever the user is working with DigitalOcean infrastructure as code — Droplets, VPCs, Cloud Firewalls, Load Balancers, DOKS/Kubernetes clusters, managed databases (Postgres/MySQL/Valkey/MongoDB/Kafka/OpenSearch), Spaces object storage, App Platform, CDN, reserved/floating IPs, DNS records, volumes, snapshots, container registry, monitoring and uptime alerts, or GradientAI — even if they don't name the provider explicitly. Also use when they mention .tf files that reference `digitalocean_*` resources, a DIGITALOCEAN_TOKEN, doctl-managed infrastructure being moved to Terraform, or ask why a DigitalOcean plan shows perpetual drift.4---56# DigitalOcean Terraform provider78Everything here is derived from the official `digitalocean/digitalocean` provider9docs at **v2.99.1**, bundled verbatim under `references/provider-docs/`. Those files10are authoritative on argument names, types, defaults, and import syntax — prefer them11over recollection, because this provider changes often and several of its resources12have near-identical siblings that take different arguments.1314## Start here1516**Never write `digitalocean_*` HCL from memory.** The failure mode is not a clear17error — it is configuration that looks correct, passes review, and then either fails18at apply or produces a plan that never converges. Confirm arguments first; it costs19one command.2021```bash22# What exists? (fuzzy, matches resources and data sources)23python3 scripts/dodocs.py list database2425# Exact arguments and attributes for one resource26python3 scripts/dodocs.py args droplet2728# Just the example, or just the import syntax29python3 scripts/dodocs.py example loadbalancer30python3 scripts/dodocs.py import database_db3132# Which resources mention an argument at all?33python3 scripts/dodocs.py search vpc_uuid3435# Data source rather than resource, when both names exist36python3 scripts/dodocs.py args kubernetes_cluster --kind data37```3839Run from the skill directory. Names work with or without the `digitalocean_` prefix,40and a wrong guess suggests near matches instead of failing silently.4142## The files4344| File | Read it when |45|---|---|46| `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. |47| `references/patterns.md` | You need more than one resource wired together — web tier, database + firewall, DOKS, Spaces + CDN, App Platform, projects, imports. |48| `references/resource-index.md` | You know the goal but not the resource name. All 131 entries with one-line purposes, grouped by subcategory. |49| `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. |50| `references/provider-docs/` | You need exact arguments. Upstream docs verbatim: `resources/<name>.md`, `data-sources/<name>.md`, plus `index.md` for provider configuration. |5152## Provider setup5354```hcl55terraform {56 required_providers {57 digitalocean = {58 source = "digitalocean/digitalocean"59 version = "~> 2.99"60 }61 }62}6364provider "digitalocean" {65 token = var.do_token # or the DIGITALOCEAN_TOKEN environment variable66}67```6869The token grants full account access, so keep it in an environment variable or a70secret manager — never committed, and never a default value on a variable.7172**Spaces is the exception that catches everyone.** Spaces resources authenticate with73S3-style keys, not the API token. Without `spaces_access_id` / `spaces_secret_key`74(or `SPACES_ACCESS_KEY_ID` / `SPACES_SECRET_ACCESS_KEY`), Spaces resources fail while75every other resource applies normally.7677Other provider arguments worth knowing: `requests_per_second` (throttle to avoid rate78limits on large applies), `http_retry_max`, `api_endpoint`, `spaces_endpoint`. Full79list in `references/provider-docs/index.md`.8081## Where things live8283The provider's own subcategories, with what each covers:8485| Area | Covers |86|---|---|87| **Networking** (33) | VPC, peering, NAT gateway, Cloud Firewall, Load Balancer, certificates, reserved IP/IPv6 (+assignments), DNS domains and records, CDN, BYOIP, Partner Network Connect |88| **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) |89| **Account** (13) | Projects and project resources, tags, SSH keys, account/region/size lookups |90| **Spaces Object Storage** (12) | Buckets, objects, policies, CORS, logging, access keys |91| **Droplets** (8) | Droplets, autoscale pools, custom images, image lookups |92| **Dedicated Inference** (8) | Dedicated inference endpoints, tokens, GPU model config, sizes |93| **NFS Storage** (7) | NFS shares, access points, snapshots, attachments |94| **Backups & Snapshots** (5) | Droplet and volume snapshots |95| **Kubernetes** (4) | DOKS clusters, node pools, version lookups |96| **Volumes Block Storage** (3) | Volumes and volume attachments |97| **Monitoring** (3) | Metric alerts, uptime checks, uptime alerts |98| **Container Registry** (3) | Registry and Docker credentials |99| **GradientAI** (2) | GradientAI agents and knowledge bases |100| **App Platform** (2) | `digitalocean_app` and its data source |101102Full per-resource breakdown with purposes: `references/resource-index.md`.103104## The traps that actually bite105106These cause the majority of broken DigitalOcean configurations. Details and fixes in107`references/gotchas.md`.1081091. **Attach exactly one way.** Volumes, reserved IPs, reserved IPv6s, and floating110 IPs can each be attached inline on the Droplet *or* through a separate111 `*_attachment` / `*_assignment` resource. Using both makes every plan show112 changes forever. Setting `volume_ids` also means Terraform claims the Droplet's113 *entire* volume set.1142. **Floating IPs are now reserved IPs.** `digitalocean_floating_ip*` is deprecated;115 use `digitalocean_reserved_ip*`.1163. **`ssh_keys` cannot change after creation.** It takes key IDs or fingerprints, not117 key material, and editing it destroys and recreates the Droplet.1184. **Don't create a DOKS cluster and its Kubernetes resources in one module.**119 Terraform evaluates provider blocks before resources exist. Split into two applies.1205. **Database `*_config` resources don't unset remote settings on destroy.** They only121 leave state. Reset values explicitly first.1226. **Managed Redis was discontinued (30 June 2025).** Use Valkey for anything new.1237. **`certificate_id` can never converge — it is a drift bug, not a style nit.**124 `digitalocean_certificate.id` is the certificate's *name*, not its UUID, so the125 field permanently disagrees with the API. Attach by `certificate_name`, and rotate126 with a new name plus `create_before_destroy`.1278. **Set `disable_lets_encrypt_dns_records = true`** when a load balancer has a128 Let's Encrypt cert and you also manage that hostname's DNS record in Terraform —129 otherwise DigitalOcean's auto-created record fights yours.1309. **`force_destroy` on a Spaces bucket defaults to `false`,** so destroy fails on a131 non-empty bucket.13210. **Projects take URNs** (`.urn`), not IDs.13311. **Singular data sources error on zero or multiple matches.** Use the plural forms134 (`digitalocean_droplets`, `digitalocean_images`, …) for lists.13512. **Renaming a volume's `filesystem_type` to `initial_filesystem_type` destroys the136 volume and its data.** The replacement is `ForceNew` and never read back, so on an137 existing volume it always diffs `null → "ext4"` and forces replacement. Delete the138 deprecated argument instead of renaming it (gotchas.md §12b).139140## Working approach141142**Writing new configuration**1431441. Find the resource — `dodocs.py list <term>` or `references/resource-index.md`.1452. Read its real arguments — `dodocs.py args <name>`. Note which are `(Required)` and146 which say "Changing this forces a new resource".1473. If more than one resource is involved, check `references/patterns.md` for the148 composed shape before assembling it yourself.1494. Scan `references/gotchas.md` for anything touching the resources you used.1505. Look up regions, sizes, and versions with data sources rather than hardcoding151 slugs — availability changes per account and region.1526. Verify: `terraform fmt`, then `terraform validate`, then `terraform plan`.153154**Reviewing or debugging existing configuration**155156- *Plan never converges / always shows changes* → check three things, in this order:157 the attach-once rule (gotchas.md §1), `certificate_id` on a load balancer or CDN158 (§8 — the field is structurally incapable of converging), and `*_config` resources159 (§5). A config can have more than one cause at once, so keep looking after the160 first hit.161- *Cleaning up deprecated arguments on a live resource* → check gotchas.md §12b162 first. The obvious fix for a volume's `filesystem_type` destroys the volume. Read163 the plan for `must be replaced` before applying, and stop on a `-/+` against164 anything holding data.165- *Unexpected destroy/recreate* → look for `ssh_keys` edits, or an argument the docs166 mark as forcing a new resource. `dodocs.py args <name>` shows these.167- *Auth failures on only some resources* → missing Spaces credentials.168- *Copied from a blog post* → check the deprecated-argument table in gotchas.md §9;169 `private_networking`, `algorithm`, `certificate_id`, and app `routes`/`cors` are170 the usual survivors.171172**Verification honesty:** `terraform validate` checks syntax and types but does not173contact the API, so it cannot confirm that a region supports a size or that a slug174exists. Only `terraform plan` (with credentials) does. Say which one you actually175ran rather than implying more coverage than you have.176177## Version note178179Bundled docs are v2.99.1 (published 2026-08-06). If the user pins a different major180or a much newer minor version, arguments may differ — check the upstream repo at181`github.com/digitalocean/terraform-provider-digitalocean` under `docs/`, or the182registry, and say so rather than assuming these docs still apply.