# Terraform and OpenTofu on DigitalOcean: Droplets, VPC and Firewall

> Deploy your first resources on DigitalOcean with Terraform or OpenTofu: API token, an SSH key, a VPC, a Droplet and a cloud firewall, with a comparison to AWS.

- Source: https://www.itwonderlab.com/terraform-digitalocean-getting-started/
- Published: 2026-06-02
- Updated: 2026-06-02
- Author: Javier Ruiz Jiménez (https://www.javierruizjimenez.com/)
- Site: IT Wonder Lab (https://www.itwonderlab.com/)

---

## DigitalOcean with Terraform

[DigitalOcean](https://www.itwonderlab.com/digitalocean/) is a cloud with a short list of services and simple pricing, popular for small and medium projects. Terraform and OpenTofu manage it with the `digitalocean` [provider](https://www.itwonderlab.com/terraform-provider/). The workflow is the same as for [AWS](https://www.itwonderlab.com/aws-terraform-tutorial-terraform-basics/), but there is much less to configure: a virtual machine is called a **Droplet**, and a Droplet can be running in a few lines.

### API token

In the DigitalOcean control panel open **API**, **Generate New Token**, and give it the scopes you need (read and write for this tutorial). The provider reads the token from an environment variable, so it stays out of the code:

```shell
$ export DIGITALOCEAN_TOKEN="<your token>"
```

### Provider

```hcl title="providers.tf"
terraform {
  required_version = ">= 1.6"

  required_providers {
    digitalocean = {
      source  = "digitalocean/digitalocean"
      version = "~> 2.0"
    }
  }
}

provider "digitalocean" {
  # the token is read from DIGITALOCEAN_TOKEN
}

variable "region" {
  type        = string
  default     = "fra1"
  description = "Region slug, for example fra1 (Frankfurt) or nyc3 (New York)"
}

variable "admin_cidr" {
  type        = string
  description = "Address range that can connect with SSH, for example your public IP as x.x.x.x/32"
}

variable "ssh_public_key_path" {
  type    = string
  default = "~/.ssh/id_ed25519.pub"
}
```

### SSH key and VPC

Upload your public key once and refer to it from the Droplet. A VPC is a private network for the Droplets of one region:

```hcl title="network.tf"
resource "digitalocean_ssh_key" "main" {
  name       = "ditwl-demo"
  public_key = file(pathexpand(var.ssh_public_key_path))
}

resource "digitalocean_vpc" "main" {
  name     = "ditwl-demo"
  region   = var.region
  ip_range = "10.10.10.0/24"
}
```

The `ip_range` must not overlap with other networks in your account. If you omit it, DigitalOcean picks a range.

### A Droplet

```hcl title="droplet.tf"
resource "digitalocean_droplet" "web" {
  name     = "ditwl-demo-web"
  image    = "ubuntu-24-04-x64"
  region   = var.region
  size     = "s-1vcpu-1gb"
  vpc_uuid = digitalocean_vpc.main.id
  ssh_keys = [digitalocean_ssh_key.main.fingerprint]
  tags     = ["web"]
}

output "web_public_ip" {
  value = digitalocean_droplet.web.ipv4_address
}
```

- `image` is a slug such as `ubuntu-24-04-x64`. `size` is a slug that sets CPU, memory and price, such as `s-1vcpu-1gb`. Not every size exists in every region.
- List the valid slugs with the command line client `doctl compute image list --public`, `doctl compute size list` and `doctl compute region list`.
- You connect as `root`: `ssh root@<ip>`. Create another user with [cloud-init](https://www.itwonderlab.com/cloud-init/) (the `user_data` argument of the Droplet) and disable root login on a real server.

### A cloud firewall

A DigitalOcean cloud firewall runs outside the Droplet. Attach it by tag, so every Droplet with the tag `web` is protected, even those created later:

```hcl title="firewall.tf"
resource "digitalocean_firewall" "web" {
  name = "ditwl-demo-web"
  tags = ["web"]

  inbound_rule {
    protocol         = "tcp"
    port_range       = "22"
    source_addresses = [var.admin_cidr]
  }

  inbound_rule {
    protocol         = "tcp"
    port_range       = "80"
    source_addresses = ["0.0.0.0/0", "::/0"]
  }

  inbound_rule {
    protocol         = "tcp"
    port_range       = "443"
    source_addresses = ["0.0.0.0/0", "::/0"]
  }

  outbound_rule {
    protocol              = "tcp"
    port_range            = "1-65535"
    destination_addresses = ["0.0.0.0/0", "::/0"]
  }

  outbound_rule {
    protocol              = "udp"
    port_range            = "1-65535"
    destination_addresses = ["0.0.0.0/0", "::/0"]
  }

  outbound_rule {
    protocol              = "icmp"
    destination_addresses = ["0.0.0.0/0", "::/0"]
  }
}
```

The outbound rules allow all outgoing traffic, so the Droplet can download packages and updates. Remove the inbound ports that you do not serve.

### Run it

```shell
$ tofu init
$ tofu plan -var admin_cidr=203.0.113.25/32
$ tofu apply -var admin_cidr=203.0.113.25/32
$ tofu destroy -var admin_cidr=203.0.113.25/32
```

Use your own public IP in `admin_cidr`, and `terraform` instead of `tofu` if you prefer HashiCorp Terraform. A Droplet is billed while it exists, even when it is powered off, so destroy test resources when you are done.

### Compare with AWS

| AWS | DigitalOcean |
|---|---|
| [EC2](https://www.itwonderlab.com/aws-ec2/) instance | Droplet |
| [VPC](https://www.itwonderlab.com/aws-vpc/) | VPC (regional, per region) |
| Security group | Cloud firewall (attached by Droplet ID or tag) |
| Key pair | SSH key |
| [S3](https://www.itwonderlab.com/aws-s3/) | Spaces |
| [EKS](https://www.itwonderlab.com/aws-eks/) | DigitalOcean Kubernetes (DOKS) |

The same provider manages Spaces, managed databases, load balancers and Kubernetes clusters. Read next [project structure](https://www.itwonderlab.com/terraform-project-structure/), [remote state](https://www.itwonderlab.com/terraform-backend/) and [best practices](https://www.itwonderlab.com/terraform-best-practices/).
