Automated summary
Guides writing OpenTofu/Terraform configurations to provision DigitalOcean resources such as Droplets, VPCs, Managed Databases, and firewalls.
Strengths
- Provides concrete HCL examples for VPCs, Droplets, cloud-init, reserved IPs, and firewalls
- Includes a table of common Droplet sizes and their use cases
- Demonstrates useful security practices such as monitoring, restricted SSH, and disabling root login
- The snippets are easy to adapt to an existing project
Limitations
- Does not cover advanced DigitalOcean products like Kubernetes, Spaces, or load balancers
- Offers no guidance on state backends, modules, or Terraform code organization
- The provided file is truncated in the dynamic IP whitelist section
Use it when you need to write or extend DigitalOcean infrastructure in OpenTofu/Terraform with core resources.
Avoid it if you target another cloud provider or need DigitalOcean resources not described here, such as DOKS, Spaces, or load balancers.
Security analysis
SafeThe skill is instructional Terraform/OpenTofu content for DigitalOcean resources; it does not run destructive commands, exfiltrate secrets, or obfuscate payloads. The declared Bash/Write/Edit tools are powerful but used for legitimate infrastructure authoring, and the examples follow normal cloud provisioning patterns with only minor hardening considerations.
- •Cloud-init example grants the deploy user passwordless sudo and docker group membership, which is a deliberate but elevated privilege choice.
- •Firewall example allows all outbound traffic and public HTTP/HTTPS; SSH is parameterized but no default restriction is shown.
- •Provider token is read from DIGITALOCEAN_TOKEN environment variable, avoiding hardcoded secrets.
Examples
Write an OpenTofu configuration for a DigitalOcean Droplet in the nyc3 region using an s-2vcpu-4gb size, attached to a VPC, with cloud-init that installs Docker and creates a deploy user.Add a DigitalOcean firewall to my existing Droplet that allows SSH only from 203.0.113.0/24 and allows HTTP and HTTPS from anywhere.Create a DigitalOcean reserved IP and assign it to my app Droplet, exposing the IP as a Terraform output.name: digitalocean-coder description: This skill guides writing DigitalOcean infrastructure with OpenTofu/Terraform. Use when provisioning Droplets, VPCs, Managed Databases, Firewalls, or other DO resources. allowed-tools: Read, Write, Edit, Grep, Glob, Bash
DigitalOcean Coder
Overview
DigitalOcean provides simple, developer-friendly cloud infrastructure. This skill covers OpenTofu/Terraform patterns for DO resources.
Provider Setup
terraform {
required_providers {
digitalocean = {
source = "digitalocean/digitalocean"
version = "~> 2.0"
}
}
}
provider "digitalocean" {
# Token from environment: DIGITALOCEAN_TOKEN
}
VPC (Virtual Private Cloud)
resource "digitalocean_vpc" "main" {
name = "${var.project}-${var.environment}-vpc"
region = var.region
ip_range = "10.10.0.0/16"
description = "VPC for ${var.project} ${var.environment}"
}
Droplets (Compute)
Basic Droplet
resource "digitalocean_droplet" "app" {
name = "${var.project}-${var.environment}-app"
region = var.region
size = var.droplet_size # s-1vcpu-1gb, s-2vcpu-4gb, etc.
image = "ubuntu-22-04-x64"
vpc_uuid = digitalocean_vpc.main.id
ssh_keys = var.ssh_key_ids
monitoring = true
ipv6 = false
tags = [var.project, var.environment]
}
Droplet with Cloud-Init
resource "digitalocean_droplet" "app" {
name = "${var.project}-app"
region = var.region
size = "s-1vcpu-2gb"
image = "ubuntu-22-04-x64"
vpc_uuid = digitalocean_vpc.main.id
ssh_keys = var.ssh_key_ids
monitoring = true
user_data = <<-EOT
#cloud-config
package_update: true
packages:
- docker.io
- docker-compose-plugin
users:
- name: deploy
groups: docker
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash
ssh_authorized_keys:
- ${var.deploy_ssh_key}
runcmd:
- systemctl enable --now docker
- sed -i 's/PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
- systemctl restart sshd
EOT
tags = [var.project]
}
Common Droplet Sizes
| Size | vCPUs | Memory | Use Case |
|------|-------|--------|----------|
| s-1vcpu-1gb | 1 | 1 GB | Testing, small apps |
| s-1vcpu-2gb | 1 | 2 GB | Small production |
| s-2vcpu-4gb | 2 | 4 GB | Medium apps |
| s-4vcpu-8gb | 4 | 8 GB | Production workloads |
| c-2 | 2 | 4 GB | CPU-intensive |
| m-2vcpu-16gb | 2 | 16 GB | Memory-intensive |
Reserved IP (Static IP)
resource "digitalocean_reserved_ip" "app" {
region = var.region
}
resource "digitalocean_reserved_ip_assignment" "app" {
ip_address = digitalocean_reserved_ip.app.ip_address
droplet_id = digitalocean_droplet.app.id
}
output "app_ip" {
value = digitalocean_reserved_ip.app.ip_address
}
Firewall
Basic Web Server Firewall
resource "digitalocean_firewall" "web" {
name = "${var.project}-web-firewall"
droplet_ids = [digitalocean_droplet.app.id]
# Inbound rules
inbound_rule {
protocol = "tcp"
port_range = "22"
source_addresses = var.ssh_allowed_ips # Restrict SSH access
}
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 rules
outbound_rule {
protocol = "tcp"
port_range = "all"
destination_addresses = ["0.0.0.0/0", "::/0"]
}
outbound_rule {
protocol = "udp"
port_range = "all"
destination_addresses = ["0.0.0.0/0", "::/0"]
}
outbound_rule {
protocol = "icmp"
destination_addresses = ["0.0.0.0/0", "::/0"]
}
}
Dynamic IP Whitelist
variable "db_allowed_ips" {
type = list(string)
default = []
description = "IPs allowed to access database directly"
}
resource "digitalocean_database_firewall" "postgres" {
cluster_id = digitalocean_database_cluster.postgres.id
# Always allow app droplet
rule {
type = "droplet"
value = digitalocean_droplet.app.id
}
# Dynamic IP whitelist for developers
dynamic "rule" {
for_each = var.db_allowed_ips
content {
type = "ip_addr"
value = rule.value
}
}
}
Managed Database
PostgreSQL Cluster
resource "digitalocean_database_cluster" "postgres" {
name = "${var.project}-${var.environment}-pg"
engine = "pg"
version = "16"
size = var.db_size # db-s-1vcpu-1gb, db-s-2vcpu-4gb
region = var.region
node_count = 1 # Increase for HA
# CRITICAL: Use private network
private_network_uuid = digitalocean_vpc.main.id
tags = [var.project, var.environment]
}
# Database firewall - restrict access
resource "digitalocean_database_firewall" "postgres" {
cluster_id = digitalocean_database_cluster.postgres.id
rule {
type = "droplet"
value = digitalocean_droplet.app.id
}
}
output "database_uri" {
value = digitalocean_database_cluster.postgres.uri
sensitive = true
}
output "database_private_uri" {
value = digitalocean_database_cluster.postgres.private_uri
sensitive = true
}
Database Sizes
| Size | vCPUs | Memory | Storage | Use Case |
|------|-------|--------|---------|----------|
| db-s-1vcpu-1gb | 1 | 1 GB | 10 GB | Development |
| db-s-1vcpu-2gb | 1 | 2 GB | 25 GB | Small production |
| db-s-2vcpu-4gb | 2 | 4 GB | 38 GB | Production |
| db-s-4vcpu-8gb | 4 | 8 GB | 115 GB | High traffic |
Redis Cluster
resource "digitalocean_database_cluster" "redis" {
name = "${var.project}-${var.environment}-redis"
engine = "redis"
version = "7"
size = "db-s-1vcpu-1gb"
region = var.region
node_count = 1
private_network_uuid = digitalocean_vpc.main.id
tags = [var.project]
}
Spaces (Object Storage)
resource "digitalocean_spaces_bucket" "assets" {
name = "${var.project}-assets"
region = var.spaces_region # nyc3, sfo3, ams3, sgp1, fra1
acl = "private" # or "public-read"
}
resource "digitalocean_spaces_bucket_cors_configuration" "assets" {
bucket = digitalocean_spaces_bucket.assets.id
region = var.spaces_region
cors_rule {
allowed_headers = ["*"]
allowed_methods = ["GET", "PUT", "POST"]
allowed_origins = ["https://${var.domain}"]
max_age_seconds = 3600
}
}
output "spaces_endpoint" {
value = digitalocean_spaces_bucket.assets.bucket_domain_name
}
DNS Records
resource "digitalocean_domain" "main" {
name = var.domain
}
resource "digitalocean_record" "app" {
domain = digitalocean_domain.main.id
type = "A"
name = "@"
value = digitalocean_reserved_ip.app.ip_address
ttl = 300
}
resource "digitalocean_record" "www" {
domain = digitalocean_domain.main.id
type = "CNAME"
name = "www"
value = "@"
ttl = 300
}
SSH Keys
# Reference existing key
data "digitalocean_ssh_key" "deploy" {
name = "deploy-key"
}
# Or create new key
resource "digitalocean_ssh_key" "deploy" {
name = "${var.project}-deploy"
public_key = file("~/.ssh/deploy.pub")
}
Complete Production Stack
locals {
name_prefix = "${var.project}-${var.environment}"
}
# VPC
resource "digitalocean_vpc" "main" {
name = "${local.name_prefix}-vpc"
region = var.region
ip_range = "10.10.0.0/16"
}
# App Server
resource "digitalocean_droplet" "app" {
name = "${local.name_prefix}-app"
region = var.region
size = var.droplet_size
image = "ubuntu-22-04-x64"
vpc_uuid = digitalocean_vpc.main.id
ssh_keys = [data.digitalocean_ssh_key.deploy.id]
monitoring = true
user_data = file("${path.module}/cloud-init.yaml")
tags = [var.project, var.environment]
}
# Static IP
resource "digitalocean_reserved_ip" "app" {
region = var.region
}
resource "digitalocean_reserved_ip_assignment" "app" {
ip_address = digitalocean_reserved_ip.app.ip_address
droplet_id = digitalocean_droplet.app.id
}
# Firewall
resource "digitalocean_firewall" "app" {
name = "${local.name_prefix}-firewall"
droplet_ids = [digitalocean_droplet.app.id]
inbound_rule {
protocol = "tcp"
port_range = "22"
source_addresses = var.ssh_allowed_ips
}
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 = "all"
destination_addresses = ["0.0.0.0/0", "::/0"]
}
outbound_rule {
protocol = "udp"
port_range = "all"
destination_addresses = ["0.0.0.0/0", "::/0"]
}
}
# Database
resource "digitalocean_database_cluster" "postgres" {
name = "${local.name_prefix}-pg"
engine = "pg"
version = "16"
size = var.db_size
region = var.region
node_count = 1
private_network_uuid = digitalocean_vpc.main.id
tags = [var.project]
}
resource "digitalocean_database_firewall" "postgres" {
cluster_id = digitalocean_database_cluster.postgres.id
rule {
type = "droplet"
value = digitalocean_droplet.app.id
}
}
# Outputs
output "app_ip" {
value = digitalocean_reserved_ip.app.ip_address
}
output "database_uri" {
value = digitalocean_database_cluster.postgres.private_uri
sensitive = true
}
Best Practices
Security
- Always use VPC for private networking
- Restrict database to droplet access only
- Use reserved IPs for consistent addressing
- Limit SSH to specific IPs via firewall
- Use cloud-init to disable root login
Cost Optimization
- Use appropriate droplet sizes
- Single-node databases for non-critical workloads
- Reserved IPs are free when assigned
- Monitor with built-in metrics
Networking
- Use
private_urifor database connections - Place all resources in same VPC
- Use firewall rules, not iptables
- Enable monitoring on droplets
Incident Postmortem Writer
DevOps
Writes structured and blameless incident postmortem reports.
Runbook Creator
DevOps
Creates clear operational runbooks for common DevOps procedures.
kube-coder preflight (run CI locally)
DevOps
Run kube-coder's full CI suite locally before pushing: helm lint, unit tests, SPA builds, vitest, and shell checks. Ensures CI green on first push.