The IaC landscape split in 2023 when HashiCorp moved Terraform from the Mozilla Public License to the Business Source License (BSL 1.1). That decision sparked OpenTofu, a community-maintained fork under the Linux Foundation that kept the original MPL-2.0 license. Ubuntu 26.04 users now have two solid choices for infrastructure as code, and both install cleanly from APT.
This guide walks through installing both Terraform and OpenTofu on Ubuntu 26.04 LTS, then covers the practical IaC workflow: creating resources, previewing and applying changes, managing state, the Docker provider, and Terragrunt as a wrapper. If you have already completed the initial server setup, you are ready to go.
Tested July 2026 on Terraform 1.15.8 and OpenTofu 1.12.5 (Ubuntu 26.04 LTS). The APT steps use codename detection, so the same commands work on Ubuntu 24.04.

Prerequisites
Before starting, make sure you have the following in place:
- Ubuntu 26.04 LTS server or desktop with root or sudo access
- Tested on: Ubuntu 26.04 LTS (Resolute Raccoon), kernel 7.0.0-15-generic
- Internet connectivity to reach the HashiCorp and OpenTofu APT repositories
- Docker installed if you plan to follow the Docker provider section (install Docker on Ubuntu 26.04)
Install Terraform on Ubuntu 26.04
Terraform ships through HashiCorp’s official APT repository. HashiCorp now publishes packages for the 26.04 resolute codename directly, so the earlier workaround of borrowing the 24.04 noble packages is no longer needed.
Install the tools needed to fetch and verify the repository key:
sudo apt-get update
sudo apt-get install -y gnupg curl
Import the HashiCorp signing key, then add the repository using your own codename so the same block works on 24.04:
. /etc/os-release
wget -qO- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com ${UBUNTU_CODENAME} main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
Refresh the package index and install Terraform:
sudo apt-get update
sudo apt-get install -y terraform
Confirm the binary and version:
terraform version
The current stable release reports:
Terraform v1.15.8
on linux_amd64
Install OpenTofu on Ubuntu 26.04
OpenTofu ships a standalone installer script that handles the GPG keys and repository setup for you. This is the method the OpenTofu project recommends.
Download the installer over a pinned TLS connection, then run it against the deb method:
curl --proto '=https' --tlsv1.2 -fsSL https://get.opentofu.org/install-opentofu.sh -o install-opentofu.sh
chmod +x install-opentofu.sh
sudo ./install-opentofu.sh --install-method deb
The script adds the OpenTofu keys, configures the APT source, and installs the tofu package. Remove the installer once it finishes:
rm -f install-opentofu.sh
Check the installed version:
tofu version
You should see the current OpenTofu release:
OpenTofu v1.12.5
on linux_amd64
If you prefer not to pipe a script into a shell, the OpenTofu deb install docs also document the manual APT repository steps, which suit locked-down CI runners.
Create Your First Terraform Configuration
The fastest way to prove both tools work is a minimal configuration. This example uses the local provider to write a file on disk, so it needs no cloud credentials.
Create a project directory:
mkdir -p ~/terraform-demo && cd ~/terraform-demo
Open the configuration file. Keep it in your own home directory, so no sudo is needed:
vim main.tf
Add the following HCL:
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
resource "local_file" "hello" {
content = "Hello from Terraform on Ubuntu 26.04!\n"
filename = "${path.module}/hello.txt"
}
output "file_path" {
value = local_file.hello.filename
}
The configuration declares a dependency on the local provider, creates a text file, and outputs its path. Save the file and move on to the workflow.
Terraform Workflow: Init, Plan, Apply, Destroy
Every Terraform project follows the same lifecycle: initialize, plan, apply, and optionally destroy. Here is each step against the config above.
Initialize the working directory
terraform init downloads the required providers and sets up the backend:
terraform init
It resolves and installs the local provider, then writes a lock file:
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/local versions matching "~> 2.5"...
- Installing hashicorp/local v2.9.0...
- Installed hashicorp/local v2.9.0 (signed by HashiCorp)
Terraform has been successfully initialized!
Preview changes with plan
terraform plan shows what will be created, changed, or destroyed without touching anything:
terraform plan
The plan ends with a one-line summary of the intended changes:
# local_file.hello will be created
+ resource "local_file" "hello" {
+ content = "Hello from Terraform on Ubuntu 26.04!\n"
+ content_base64sha256 = (known after apply)
+ content_base64sha512 = (known after apply)
+ content_md5 = (known after apply)
+ content_sha1 = (known after apply)
+ content_sha256 = (known after apply)
+ content_sha512 = (known after apply)
+ directory_permission = "0777"
+ file_permission = "0777"
+ filename = "./hello.txt"
+ id = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Apply the configuration
Run terraform apply to create the resources. The -auto-approve flag skips the confirmation prompt, which suits automation, but review the plan first in production:
terraform apply -auto-approve
Terraform creates the file and prints the output:
local_file.hello: Creating...
local_file.hello: Creation complete after 0s [id=2138eaf75c666a9671d08eaebbf8d80bb2347ad1]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
file_path = "./hello.txt"
Confirm the file landed on disk:
cat hello.txt
This prints:
Hello from Terraform on Ubuntu 26.04!
Tear down resources
terraform destroy removes everything Terraform manages. This is the cleanup step:
terraform destroy -auto-approve
The output confirms the resource is gone:
local_file.hello: Destroying... [id=2138eaf75c666a9671d08eaebbf8d80bb2347ad1]
local_file.hello: Destruction complete after 0s
Destroy complete! Resources: 1 destroyed.
Run the Same Example with OpenTofu
OpenTofu uses identical HCL and the same provider registry. The command is tofu instead of terraform, but the workflow is the same.
Create a separate directory and reuse the same configuration:
mkdir -p ~/tofu-demo && cd ~/tofu-demo
cp ~/terraform-demo/main.tf .
Initialize the directory:
tofu init
OpenTofu pulls the same provider from the registry:
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/local versions matching "~> 2.5"...
- Installing hashicorp/local v2.9.0...
- Installed hashicorp/local v2.9.0 (signed, key ID 0C0AF313E5FD9F80)
OpenTofu has been successfully initialized!
Apply the configuration:
tofu apply -auto-approve
The result matches Terraform exactly:
local_file.hello: Creating...
local_file.hello: Creation complete after 0s [id=2138eaf75c666a9671d08eaebbf8d80bb2347ad1]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
file_path = "./hello.txt"
Clean up when done:
tofu destroy -auto-approve
The identical resource ID across both tools comes from the deterministic content hash, not a coincidence. The lock file and state file formats are compatible between Terraform and OpenTofu, though mixing both against the same state file is not recommended.
Docker Provider Example
A more practical test manages Docker containers from Terraform. This section needs Docker installed on the host.
Create a new project directory:
mkdir -p ~/docker-demo && cd ~/docker-demo
Open the configuration file:
vim main.tf
Add a configuration that pulls an Nginx image and runs a container:
terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0"
}
}
}
provider "docker" {}
resource "docker_image" "nginx" {
name = "nginx:alpine"
keep_locally = false
}
resource "docker_container" "nginx" {
image = docker_image.nginx.image_id
name = "terraform-nginx"
ports {
internal = 80
external = 8080
}
}
output "container_id" {
value = docker_container.nginx.id
}
output "container_name" {
value = docker_container.nginx.name
}
Initialize and apply:
terraform init
terraform apply -auto-approve
Terraform pulls the image and starts the container:
docker_image.nginx: Creating...
docker_image.nginx: Creation complete after 3s [id=sha256:4a73073bd557...nginx:alpine]
docker_container.nginx: Creating...
docker_container.nginx: Creation complete after 1s [id=5b9dd6a3f84c...]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:
container_id = "5b9dd6a3f84c2bf338f8400ca81423e446a324ac6f516afd8c9c2a4cf6325a76"
container_name = "terraform-nginx"
Confirm the container is up with docker ps:
docker ps
The container is listed and serving on port 8080:
CONTAINER ID IMAGE STATUS PORTS NAMES
5b9dd6a3f84c 4a73073bd557 Up 17 seconds 0.0.0.0:8080->80/tcp terraform-nginx
Check that Nginx responds:
curl -s http://localhost:8080 | head -4
You get the Nginx welcome page HTML:
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
State Management Commands
Terraform tracks every managed resource in a state file (terraform.tfstate). These commands inspect that state.
List everything Terraform is tracking:
terraform state list
For the Docker example, this returns both resources:
docker_container.nginx
docker_image.nginx
Inspect a single resource in detail:
terraform state show docker_container.nginx
The output opens with the resource header and its attributes:
# docker_container.nginx:
resource "docker_container" "nginx" {
attach = false
command = [
"nginx",
"-g",
"daemon off;",
]
container_read_refresh_timeout_milliseconds = 15000
cpu_shares = 0
...
}
The state file is the source of truth for what Terraform manages. Never edit it by hand. When you finish the Docker demo, destroy the resources:
terraform destroy -auto-approve
Formatting and Validation
Both tools ship built-in formatting and validation that catch issues before plan or apply.
Check whether your HCL follows the canonical style:
terraform fmt -check
No output means the files are already formatted. Drop the -check flag to rewrite files in place. Next, validate syntax and internal consistency:
terraform validate
A valid configuration returns:
Success! The configuration is valid.
The same commands work with OpenTofu as tofu fmt and tofu validate. Running fmt in CI catches style drift before code review. If you write custom providers in Go on Ubuntu 26.04, these commands validate the HCL that consumes them too.
Install Terragrunt
Terragrunt is a thin wrapper around Terraform and OpenTofu that reduces duplication across multi-environment deployments. It adds DRY backend configuration, dependency orchestration between modules, and before/after hooks.
Fetch the latest release and install the binary:
TGVER=$(curl -sL https://api.github.com/repos/gruntwork-io/terragrunt/releases/latest | grep tag_name | head -1 | sed 's/.*"v\([^"]*\)".*/\1/')
echo "Installing Terragrunt v${TGVER}"
sudo wget -q "https://github.com/gruntwork-io/terragrunt/releases/download/v${TGVER}/terragrunt_linux_amd64" -O /usr/local/bin/terragrunt
sudo chmod +x /usr/local/bin/terragrunt
Verify the installed version:
terragrunt --version
The output confirms the release:
terragrunt version v1.1.1
Terragrunt works with either engine. The v1 line calls tofu by default, so OpenTofu users need no extra configuration. If you installed only Terraform, point Terragrunt at it with terraform_binary = "terraform" in your terragrunt.hcl, set the environment variable TG_TF_PATH=terraform (the v1.0 CLI redesign renamed the old TERRAGRUNT_TFPATH), or pass --tf-path terraform. Configuration tools like Ansible pair well with Terragrunt for post-provisioning steps.
Terraform vs OpenTofu: Which One to Use
Both tools share the same roots (OpenTofu forked from Terraform 1.5.7) and stay highly compatible. The decision comes down to licensing requirements and organizational preferences.
| Criteria | Terraform | OpenTofu |
|---|---|---|
| License | BSL 1.1 (Business Source License) | MPL-2.0 (Mozilla Public License) |
| Governance | HashiCorp (IBM) | Linux Foundation |
| Provider registry | registry.terraform.io | Same registry, plus the OpenTofu registry |
| State file format | Compatible | Compatible |
| HCL syntax | Identical | Identical, plus state encryption |
| CLI command | terraform | tofu (drop-in replacement) |
| Unique features | HCP Terraform (formerly Terraform Cloud), Sentinel policies | Client-side state encryption, OCI registry for providers and modules |
| Commercial use | Restricted for competing products | No restrictions |
| Enterprise support | HashiCorp / IBM | Multiple vendors (Spacelift, env0, Scalr) |
If your organization builds a product that competes with HashiCorp offerings, the BSL license restricts you from using Terraform. OpenTofu carries no such restriction. For most internal infrastructure teams, both tools work equally well. OpenTofu’s client-side state encryption is a genuine advantage when you store state in S3 or GCS and want encryption at the application layer rather than relying only on the backend’s server-side encryption.
One practical point: if HCP Terraform (formerly Terraform Cloud) or Terraform Enterprise is already in your stack, moving to OpenTofu means giving up those managed services. If you are starting fresh or self-managing state backends, OpenTofu gives you the same capabilities without the license concern.