This site contains affiliate links — we may earn a commission if you sign up through them.

Terraform on DigitalOcean 2026 — Infrastructure as Code Guide

คู่มือประหยัดเวลาสำหรับจัดการ infrastructure บน DigitalOcean ด้วย Terraform โดยเฉพาะสำหรับทีม นี่จะช่วยให้ infrastructure reproducible, trackable ผ่าน Git, และลดความผิดพลาดจากการคลิกเสร็จสิ้น

Terraform on DigitalOcean 2026 — Infrastructure as Code Guide

Terraform is an open-source Infrastructure as Code (IaC) tool from HashiCorp that lets you define your entire DigitalOcean infrastructure—servers, networks, databases, load balancers—as declarative code files instead of clicking through the Control Panel. If you're managing infrastructure at scale, whether solo or with a team, Terraform dramatically reduces manual setup time, enables version control for your infrastructure, and makes disaster recovery as simple as running terraform apply. This guide walks through setting up the official DigitalOcean provider, creating real resources (Droplets, VPC, Managed Database, Load Balancer), managing state files, and team best practices that prevent common mistakes.

What is Terraform and Why Use It with Cloud

Terraform is an Infrastructure as Code (IaC) tool from HashiCorp that lets you write your infrastructure—servers, networks, databases, and more—as configuration files in HCL (HashiCorp Configuration Language) instead of clicking through the DigitalOcean Control Panel one resource at a time. It works declaratively: you declare what your final infrastructure should look like, and Terraform calculates what needs to be created, modified, or deleted to match your declaration. This differs from imperative scripts where you must write each step manually. For teams using DigitalOcean, the benefits are clear and compelling. First is reproducibility: your .tf files committed to Git become the single source of truth for your entire infrastructure. Anyone on the team can clone the repo and run terraform apply to recreate the same environment—dev, staging, or production—identically every time. Second is version control: every infrastructure change is tracked as a commit, reviewable via pull request, and rollbackable like any code change. Third is reducing human error: clicking to create a Droplet by hand leaves room for mistakes like forgetting SSH keys or firewall rules, but Terraform guarantees the same correct result every apply. Fourth is disaster recovery: if a Droplet or Load Balancer is accidentally deleted or an entire region fails, you can apply the same .tf file to another region and restore your system far faster than manual setup. Terraform supports DigitalOcean through the official provider called digitalocean/digitalocean, published on the Terraform Registry and covering nearly every major service: Droplets, VPC, Load Balancer, Managed Database, Volumes, Firewall, Domain/DNS, and Kubernetes. This means you can manage your entire infrastructure stack with one tool instead of switching between the CLI (doctl), API, and web dashboard.

  1. Declarative config: state your desired outcome, not step-by-step commands like shell scripts
  2. Version control like code — commit, diff, code review via pull request
  3. Reproducible: clone the repo and terraform apply to get the same environment every time
  4. Single provider (digitalocean/digitalocean) manages Droplets, VPC, Database, Load Balancer, and more
  5. Reduces human error risk and speeds disaster recovery when you need to rebuild fast

Install Terraform and DigitalOcean Provider

Worth highlighting here — installing Terraform on your local machine is straightforward and platform-specific. On macOS, the easiest way is Homebrew with brew tap hashicorp/tap && brew install hashicorp/tap/terraform. On Linux distributions supporting snap, use sudo snap install terraform --classic, or add the HashiCorp apt repository and install via apt-get install terraform. On Windows, Chocolatey provides choco install terraform. After installation, verify the binary works with terraform version. Next, declare the DigitalOcean provider in a config file to tell Terraform which plugin to download from the Terraform Registry. Create a file like providers.tf and add this block: terraform {\n required_providers {\n digitalocean = {\n source = \"digitalocean/digitalocean\"\n version = \"~> 2.0\"\n }\n }\n}\n\nprovider \"digitalocean\" {\n token = var.do_token\n} Pinning the version with ~> 2.0 prevents minor updates with breaking changes from sneaking in during the next terraform init. The critical step is securing your API token: never hardcode it directly in .tf files—they typically get committed to Git. Generate a Personal Access Token from cloud.digitalocean.com/account/api/tokens (with read/write permissions) and declare it as a variable: variable \"do_token\" {\n type = string\n sensitive = true\n} Pass the token at runtime via environment variable export TF_VAR_do_token=\"dop_v1_xxxxx\" or store it in terraform.tfvars added to .gitignore so it never reaches the repo. Finally, run terraform init to download the provider plugin into the .terraform/ folder and you're ready to start.

Create a Droplet with .tf — Real Example

Once the provider is ready, write a resource block to create an actual Droplet. This example creates a s-1vcpu-1gb Droplet (1 GiB RAM, 1 vCPU, 25 GB SSD, $6/month) in the sgp1 region (Singapore), which offers the lowest latency for Thailand users: resource \"digitalocean_droplet\" \"web\" {\n image = \"ubuntu-24-04-x64\"\n name = \"web-01\"\n region = \"sgp1\"\n size = \"s-1vcpu-1gb\"\n ssh_keys = [var.ssh_fingerprint]\n tags = [\"web\", \"production\"]\n} The image field uses the OS image slug (e.g., ubuntu-24-04-x64), and size uses the Droplet plan slug—find these via doctl compute size list or the provider docs. For a beefier workload like 4 GiB RAM/2 vCPU/80 GB SSD at $24/month, simply change size to s-2vcpu-4gb without touching anything else. The ssh_keys field references the fingerprint of an SSH key already uploaded to your DigitalOcean account (via dashboard or digitalocean_ssh_key resource) so login works immediately after boot without passwords. After writing the config, follow the three-step process: first run terraform init to load the provider (if not already done), then terraform plan to show Terraform's computed changes before applying—watch for + create entries. Read the plan carefully every time before proceeding. Finally, run terraform apply, type yes to confirm, and Terraform calls the DigitalOcean API to create the Droplet and returns its public IP. To automatically display the IP after apply succeeds, add an output block to a file like outputs.tf: output \"web_ip\" {\n value = digitalocean_droplet.web.ipv4_address\n} Now every successful apply will show the IP on screen without needing to open the dashboard.

Key takeaway: image = \"ubuntu-24-04-x64\" and size = \"s-1vcpu-1gb\" ($6/month, 1 GiB RAM/1 vCPU/25 GB SSD)

Manage VPC, Database, Load Balancer via Terraform

Terraform isn't limited to Droplets—it covers nearly every DigitalOcean service, letting you declare networks, databases, and load balancers in the same files and link them together via references. Start with VPC, a free private network that DigitalOcean provides with no per-instance charge, perfect for separating environments (dev/prod) with zero cross-visibility: resource \"digitalocean_vpc\" \"prod\" {\n name = \"prod-vpc\"\n region = \"sgp1\"\n} Attach a Droplet to this VPC instantly by adding vpc_uuid = digitalocean_vpc.prod.id to the digitalocean_droplet block—this is a key Terraform strength: one resource can reference another's attributes directly without manually copying IDs. For a Managed PostgreSQL database at the Basic tier (1 vCPU/1 GiB RAM, $15.15/month) write: resource \"digitalocean_database_cluster\" \"pg\" {\n name = \"prod-postgres\"\n engine = \"pg\"\n version = \"16\"\n size = \"db-s-1vcpu-1gb\"\n region = \"sgp1\"\n node_count = 1\n private_network_uuid = digitalocean_vpc.prod.id\n} Setting private_network_uuid to the same VPC means the database has no public IP, accessible only from resources in that same VPC—this is the recommended security practice. For a Load Balancer starting at $12/month, which distributes traffic across tagged Droplets: resource \"digitalocean_loadbalancer\" \"web_lb\" {\n name = \"web-lb\"\n region = \"sgp1\"\n droplet_tag = \"web\"\n\n forwarding_rule {\n entry_port = 443\n entry_protocol = \"https\"\n target_port = 80\n target_protocol = \"http\"\n }\n\n healthcheck {\n port = 80\n protocol = \"http\"\n }\n} Using droplet_tag instead of individual Droplet IDs means when you scale up and add new Droplets with the same tag (via multiple digitalocean_droplet resources or count), the Load Balancer automatically enrolls them without editing the Load Balancer config itself.

State Files and terraform plan/apply/destroy

At the heart of Terraform is the state file, a JSON file called terraform.tfstate that Terraform creates automatically after your first apply. It maps resources you declare in .tf files to their actual IDs on DigitalOcean. Every terraform plan or terraform apply compares three states: your .tf config, the state file, and the live state on DigitalOcean (via API refresh) to determine what to create/update/delete. The three main commands for daily work are terraform plan, a dry-run that shows changes without applying them—perfect for code review before merging a pull request; terraform apply, which executes the plan and updates the state file; and terraform destroy, which deletes all resources in the state. Destroy is dangerous because it actually removes things—use it only for test environments or when truly retiring a project, always running terraform plan -destroy first to confirm. The critical warning is never commit terraform.tfstate to Git—it stores every resource attribute including secrets like database passwords in plain text. Add both terraform.tfstate and terraform.tfstate.backup to .gitignore without exception. For team workflows, storing state locally is problematic because other team members won't see the latest state, risking duplicate or conflicting applies. Use a remote backend like DigitalOcean Spaces (S3-compatible): terraform {\n backend \"s3\" {\n endpoints = { s3 = \"https://sgp1.digitaloceanspaces.com\" }\n bucket = \"team-terraform-state\"\n key = \"prod/terraform.tfstate\"\n region = \"us-east-1\"\n skip_credentials_validation = true\n skip_region_validation = true\n }\n} With a remote backend, the entire team applies from a single shared state file, eliminating state drift between machines.

Best Practice Guidelines for Teams

As projects grow with multiple environments or multiple team members, several practices keep your Terraform codebase manageable and safe. First, separate state by environment so dev/staging/production don't share one state file. Use Terraform Workspaces (terraform workspace new production) or split into directories with different backend keys like key = \"dev/terraform.tfstate\" vs. key = \"prod/terraform.tfstate\"—the folder approach is often clearer and reduces accidental cross-environment applies. Second, use modules to eliminate repetition. If you have a pattern like "Droplet + Firewall + Reserved IP" that repeats across services, wrap it as a module and call it with different variables: module \"api_server\" {\n source = \"./modules/droplet-stack\"\n name = \"api\"\n size = \"s-2vcpu-4gb\"\n region = \"sgp1\"\n} Now fixing the pattern once updates every service using that module without copy-pasting. Third, handle secrets carefully. Never store API tokens or database passwords in .tf or .tfvars files committed to the repo. Pass them via environment variables in your CI/CD pipeline or use a dedicated secrets manager. Always set sensitive = true on secret variables and outputs so they don't leak into terraform plan or CI logs. Fourth, integrate into your CI/CD pipeline rather than letting each person apply from their laptop. Configure pull requests to auto-run terraform plan and post the results as a comment for team review, then apply only after merge to the main branch via a CI job with apply permissions. This ensures all infrastructure changes go through code review like application code, preventing solo applies that bypass team oversight. Finally, enforce naming conventions and tags on every resource—tags like tags = [\"env:production\", \"team:backend\"] make tracking, cost allocation, and Load Balancer droplet_tag filtering much easier when projects scale to many resources.

  1. Separate state per environment using Terraform Workspaces or folder-based backend keys
  2. Wrap repeated patterns into modules instead of copy-pasting resource blocks
  3. Never commit secrets to the repo — use environment variables/secret manager and set sensitive = true

Common Mistakes and How to Fix Them

One thing that surprised us: when using Terraform with DigitalOcean in production, certain problems appear repeatedly and deserve advance solutions. The first is state drift: when someone modifies a resource via the dashboard or doctl directly (e.g., resizes a Droplet or tweaks firewall rules) without going through Terraform, the state file becomes stale. Symptoms include terraform plan showing diffs you didn't touch in .tf files. Check the current state with terraform state show digitalocean_droplet.web and compare to live. To sync state to reality without re-applying, run terraform apply -refresh-only (or terraform refresh on older versions). Better yet, prevent drift by establishing a team rule: only modify Terraform-managed resources through .tf files, never via the dashboard. Second is provider authentication failure with errors like "Unable to authenticate you" or "401 Unauthorized." Common causes include an expired or revoked token, a token with read-only permissions when the config needs write access, or mismatched environment variable names—e.g., declaring variable \"do_token\" but setting export TF_VAR_token=... (missing the do_ prefix). Fix: generate a fresh Personal Access Token with read/write permissions and double-check the TF_VAR_ prefix matches your variable name exactly. Third is dependency ordering: normally Terraform auto-detects order from resource references like vpc_uuid = digitalocean_vpc.prod.id, but some resources lack direct attribute links while one must complete before another. Use depends_on explicitly: depends_on = [digitalocean_firewall.web_fw] to force sequencing. Fourth is import mismatch: after successfully running terraform import to pull an existing resource into state, running terraform plan still shows diffs because your .tf code doesn't match the live attributes (tags, region, size don't match). Check actual state with terraform show, then edit .tf fields until terraform plan reports "No changes."

Best Practices (Advanced)

Beyond environment separation and modules, several practices keep Terraform on DigitalOcean stable at scale, especially with multiple team members and concurrent applies. First, state locking: S3-compatible backends on DigitalOcean Spaces lack native locking like AWS S3 + DynamoDB provide, meaning two people could apply simultaneously and corrupt state. Mitigate by enforcing CI-only applies with strict concurrency control—set GitHub Actions concurrency: group: terraform-prod to queue jobs, preventing parallel runs. Never let developers apply freely from laptops. Second, lock provider versions with the .terraform.lock.hcl file that Terraform creates after terraform init. Unlike terraform.tfstate, this file should be committed to Git—it pins provider checksums so every team member and CI runs the exact same provider version, preventing surprises from new minor versions. Third, enforce quality checks before apply: run terraform fmt -check to verify formatting, terraform validate for syntax and type correctness, and optionally add open-source tools like tflint or tfsec to the pipeline for best-practice and security scanning (e.g., catching accidentally public access on private resources). Fourth, structure your repo clearly from the start. Separate reusable modules from environment-specific code: infra/\n modules/\n droplet-stack/\n envs/\n dev/\n main.tf\n backend.tf\n prod/\n main.tf\n backend.tf Each environment has its own backend.tf pointing to different state files (as discussed earlier) and calls the shared module with different variable values. Add descriptions to every variable so teammates understand purpose without reading the code: variable \"droplet_size\" {\n type = string\n description = \"Droplet size slug e.g. s-1vcpu-1gb\"\n default = \"s-1vcpu-1gb\"\n} This structure lets you scale to many environments or services without copy-paste and dramatically reduces the risk of applying to the wrong environment by mistake.

Get $200 Free Credit →

Frequently Asked Questions

How is Terraform different from doctl or calling the API directly?
doctl and the API work well for one-off commands or ad-hoc scripts, while Terraform suits managing your entire infrastructure stack declaratively in a reproducible, reviewable, version-controlled way. All three use the DigitalOcean API v2 under the hood, but differ in abstraction level.
Does using Terraform with DigitalOcean incur extra costs?
Terraform itself is open-source and free. The only costs are for the actual resources you create on DigitalOcean—Droplets, Databases, Load Balancers, etc.—billed at normal DigitalOcean rates.
I already have Droplets created manually via dashboard. Can I import them into Terraform later?
Yes, use terraform import digitalocean_droplet.web DROPLET_ID to pull an existing resource into state. You must then write a .tf file with attributes matching the live resource—Terraform won't auto-generate the entire config (though newer versions have partial import block generation).
If apply fails partway through, is the state corrupted?
Normally Terraform only updates state for resources that completed successfully. If apply fails, run terraform plan again to see the current state vs. config, fix the underlying issue, and apply again—no need to delete or reset state.
Should I use Terraform Cloud (by HashiCorp) instead of a remote backend?
It depends on team size. Small teams can use DigitalOcean Spaces (S3-compatible) as shown here—no extra cost, fully functional. Larger teams wanting full state locking, policy-as-code, and a UI for approvals may evaluate Terraform Cloud or Terraform Enterprise separately.
What if my state file is lost or leaked from the repo?
If state is lost and resources still exist on DigitalOcean, recover by running terraform import on each resource one by one, which is time-consuming if you have many. This is why remote backends with versioning (e.g., DigitalOcean Spaces with object versioning enabled) matter from day one—you can restore a prior state snapshot instantly if needed.