The *arr Stack, part 2: First Linode VM with Terraform

Posted on Feb 3, 2026
tl;dr:

The Goal

Stand up one Linode VM using only Terraform, with:

  • SSH key access (no password nonsense)
  • output the public IP so I can actually log in
  • nothing clever yet (cloud-init comes next)

Also: Linodes cost money while they exist. Don’t forget to destroy stuff when you’re done playing.

Prereqs (boring but required)

  • Terraform installed (anything modern, I’m not here to fight versions)
  • A Linode (Akamai) account
  • An SSH keypair on your machine

Provider-wise, I’m using the Linode Terraform Provider v3+. v3 is the active line and is what you want for “future feature support” type reasons. (akamai.com)

Step 1: Make a Linode API Token

Go to Cloud Manager and create a Personal Access Token (PAT) with only the permissions you need (for now: Compute Instances is enough, but I usually just do Compute + Firewalls so I don’t forget later). (techdocs.akamai.com)

Do not commit this token anywhere. I’m not your mum, but I will judge you.

How I wire the token into Terraform

The provider supports reading LINODE_TOKEN from your shell environment. That’s the path of least regret. (registry.terraform.io)

export LINODE_TOKEN="paste-it-here"

(Yes, your shell history exists. Be sensible.)

Step 2: Scaffold the repo (minimal edition)

I’m keeping this part tiny on purpose.

repo/
  infra/
    envs/
      dev/
        main.tf
        variables.tf
        outputs.tf
        versions.tf
        terraform.tfvars.example
  .gitignore

.gitignore

Terraform leaves little footprints everywhere. Ignore them.

# Terraform
.terraform/
*.tfstate
*.tfstate.*
*.tfvars
crash.log
crash.*.log
.terraform.lock.hcl

I ignore .terraform.lock.hcl in my personal stuff sometimes. In “enterprise mode” you probably commit it. Do what fits.

Step 3: Pin provider versions

versions.tf:

terraform {
  required_version = ">= 1.0.0"

  required_providers {
    linode = {
      source  = "linode/linode"
      version = "~> 3.0"
    }
  }
}

v3 is the line we want. (akamai.com)

Step 4: Variables

variables.tf:

variable "region" {
  type        = string
  description = "Where the VM lives (e.g. eu-west)"
}

variable "type" {
  type        = string
  description = "Linode plan type (e.g. g6-nanode-1, g6-standard-2)"
}

variable "image" {
  type        = string
  description = "Linode image slug (usually starts with linode/)"
}

variable "label" {
  type        = string
  description = "Instance label"
}

variable "authorized_key" {
  type        = string
  description = "Your SSH public key (single-line, starts with ssh-ed25519 or ssh-rsa)"
}

Notes:

Step 5: Create the instance

main.tf:

provider "linode" {
  # token is read from LINODE_TOKEN env var
}

resource "linode_instance" "arr" {
  label  = var.label
  region = var.region
  type   = var.type
  image  = var.image

  authorized_keys = [var.authorized_key]

  tags = ["arr", "terraform", "dev"]
}

Root password?

Every Linode has a root password behind the scenes, but root_pass is optional in Terraform. If you omit it, Linode will generate one (and it won’t be stored in state). (linode.com)

I’m using SSH keys only, so I’m omitting it.

Step 6: Outputs (so we can actually use the thing)

outputs.tf:

output "ipv4" {
  value       = linode_instance.arr.ip_address
  description = "Public IPv4 address"
}

output "ipv6" {
  value       = linode_instance.arr.ipv6
  description = "IPv6 address (if available)"
}

Step 7: tfvars (example only)

terraform.tfvars.example:

region         = "eu-west"
type           = "g6-standard-1"
image          = "linode/ubuntu24.04"
label          = "arr-dev-1"
authorized_key = "ssh-ed25519 AAAA... you@machine"

Ubuntu 24.04 has a public image slug linode/ubuntu24.04 (handy default). (linode.com)

Copy it to terraform.tfvars locally (remember: .tfvars is ignored).

Step 8: Apply it

From infra/envs/dev:

terraform init
terraform fmt -recursive
terraform validate
terraform plan
terraform apply

When it’s up:

terraform output
ssh root@$(terraform output -raw ipv4)

If SSH doesn’t work: it’s usually one of

  • wrong key pasted
  • you’re on the wrong IP
  • you created it… and immediately forgot you haven’t done firewalls yet (next part 👀)

Cleanup (seriously, do it)

If this is just a test VM:

terraform destroy

Where this goes next

Right now, this VM is basically an empty apartment: four walls, no furniture.

Next part is where we add the “first day essentials” using cloud-init:

  • create a non-root user
  • patch baseline packages
  • install Docker
  • create /srv/appdata, /srv/downloads, /srv/media
  • leave the machine in a predictable state every time

Linode’s Metadata service + user-data is how we’ll feed cloud-init that bootstrap config. (techdocs.akamai.com)