Automation10 min read

OpenTofu: manage your VPS fleet with Infrastructure as Code

You manage ten VPS, then twenty, and each new server requires the same manual checklist: order, network, Docker, reverse proxy, certificate. One step missed in the sequence, and a delivery fails. OpenTofu, the open source fork of Terraform hosted under the CNCF, reverses this logic: you describe the target state of your infrastructure in HCL, run `tofu apply`, and the tool calculates the delta between what exists and what should exist. This guide covers VPS provisioning — the creation, update and destruction of servers — as a complement to the system configuration that Ansible already handles.

Why OpenTofu instead of continuing with manual SSH

Up to five or six clients, manual management holds: you remember what runs where, and the setup checklist is memorized. Beyond that, memory becomes an operational risk. A manually provisioned VPS has no readable state: without connecting, you do not know whether the firewall is configured, whether Docker has the expected version, or whether this server is still part of the rotation.

OpenTofu solves a different problem than Ansible. Ansible manages the *configuration* of a server that already exists — what runs on it, which files are present, which services are started. OpenTofu manages the *lifecycle* of the server itself: creation, attribute updates, destruction. It maintains a state file (terraform.tfstate) that knows exactly what is provisioned and what is not. The two tools are complementary: OpenTofu provisions, Ansible configures.

In August 2023, HashiCorp changed the Terraform license from MPL-2.0 to BUSL-1.1, a non-free license that restricts certain commercial uses. The community responded by creating OpenTofu, an HCL-compatible fork for versions prior to Terraform 1.6, now hosted under the CNCF (Cloud Native Computing Foundation) and the Linux Foundation. The command is tofu, not terraform, but .tf files and the logic are identical.

What you gain with a declarative state

  • Reliable inventory — the terraform.tfstate file tells you exactly which servers exist, with which IPs and attributes, without connecting to each one.
  • Reproducibility — a new client or project gets the same VPS configured the same way, from the same .tf file versioned in Git.
  • Clean destructiontofu destroy removes resources in the right order, without leaving orphaned servers that keep being billed.
  • Readable difftofu plan shows exactly what will be created, modified or destroyed before acting, like a git diff of your infrastructure.
  • Ansible complementarity — OpenTofu creates the VPS and sets metadata (SSH key, name, network); Ansible takes over to deploy Docker, Nginx and your applications.
  • Versionable and auditable — your infrastructure becomes a Git repository with commits, code reviews and a history of changes.
  • Sustainable open source license — OpenTofu is under MPL-2.0, without commercial restrictions, with community governance under the CNCF.

Prerequisites before you start

This guide assumes you have a VPS with root access and a dedicated IPv4 to run your workloads, and a development workstation (macOS, Linux or Windows with WSL2) from which you run tofu. OpenTofu itself does not run on the target VPS: it executes locally and talks to the provider API or the server's Docker daemon.

To follow the examples, you need: OpenTofu installed locally (see opentofu.org/docs/intro/install), API access or SSH credentials to the target VPS, and Git to version your .tf files. No additional dependencies are required — OpenTofu downloads providers itself at tofu init.

To control Docker on a remote VPS via OpenTofu, the server's Docker daemon must listen on its Unix socket (default) or on a secured TCP port. The kreuzwerker/docker provider connects to this socket via SSH or TCP.

Setting up OpenTofu for a VPS fleet

01

Install OpenTofu locally

Visit opentofu.org/docs/intro/install for instructions based on your OS. On macOS with Homebrew: brew install opentofu. On Debian/Ubuntu: the official OpenTofu repository provides the opentofu package. Verify the installation with tofu version — the command should return the installed version.

02

Structure your Infrastructure as Code project

Create a dedicated directory and four standard files.

# main.tf — main resources
# variables.tf — variable declarations
# outputs.tf — values exposed after apply
# terraform.tfvars — concrete values (gitignore if secrets)

This structure is not required by OpenTofu, but it is the widely adopted convention: main.tf holds the resources, variables.tf declares types and default values, outputs.tf exposes what Ansible or another tool needs to read after provisioning (server IP, hostname…), and terraform.tfvars contains the concrete values you do not want to hard-code in main.tf.

03

Declare the provider and generate an SSH key

OpenTofu installs required providers at tofu init. The hashicorp/tls provider generates an SSH key pair locally, eliminating manual key management.

terraform {
  required_providers {
    tls = {
      source  = "hashicorp/tls"
      version = "~> 4.0"
    }
  }
}

resource "tls_private_key" "vps_key" {
  algorithm = "ED25519"
}

output "private_key_pem" {
  value     = tls_private_key.vps_key.private_key_pem
  sensitive = true
}

Run tofu init to download the provider, then tofu apply to generate the key. Retrieve it with tofu output -raw private_key_pem > ~/.ssh/vps_key && chmod 600 ~/.ssh/vps_key.

04

Provision the VPS with null and local-exec

If your VPS provider does not have an official OpenTofu provider, the hashicorp/null provider with a local-exec allows you to run a local command (a curl API call, a shell script) and model the resource in the state.

resource "null_resource" "vps_provision" {
  triggers = {
    server_name = var.server_name
  }

  provisioner "local-exec" {
    command = <<EOT
      curl -s -X POST https://api.your-provider.com/v1/servers \
        -H "Authorization: Bearer ${var.api_token}" \
        -d '{"name": "${var.server_name}", "image": "ubuntu-22.04"}'
    EOT
  }
}

This pattern suits a transitional phase. If your provider exposes a REST API, a shell script called via local-exec is sufficient to create the server and write its IP to a file that outputs.tf will then expose.

05

Control Docker on the VPS with the kreuzwerker provider

Once the VPS is provisioned and Docker installed (by Ansible, typically), the kreuzwerker/docker provider lets you declare containers, networks and volumes in OpenTofu.

terraform {
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 3.0"
    }
  }
}

provider "docker" {
  host = "ssh://root@${var.server_ip}:22"
}

resource "docker_container" "app" {
  name  = "my-app"
  image = docker_image.app.image_id
}

resource "docker_image" "app" {
  name = "nginx:alpine"
}

The provider connects to the VPS Docker daemon via SSH — no additional TCP port to open. The stable provider version is available on the Terraform Registry.

06

Version the state for a team

For solo development, the local state (terraform.tfstate) is sufficient. In a team or CI/CD context, two developers running tofu apply simultaneously corrupt the state. The solution is a remote backend with locking.

OpenTofu natively supports S3 (AWS, self-hosted MinIO) and GitLab Managed Terraform State. With a MinIO bucket on a VPS:

terraform {
  backend "s3" {
    bucket                      = "tofu-state"
    key                         = "production/terraform.tfstate"
    region                      = "eu-west-1"
    endpoint                    = "https://minio.your-domain.com"
    skip_credentials_validation = true
    skip_metadata_api_check     = true
    skip_region_validation      = true
    force_path_style            = true
  }
}

Locking is automatic: if a tofu apply is running, a second one is rejected until the first completes.

07

Integrate OpenTofu into your CI/CD pipeline

A typical Woodpecker CI or Forgejo Actions pipeline runs tofu plan on every pull request (for human review of the infrastructure diff) and tofu apply on merge to the main branch.

steps:
  - name: tofu-plan
    image: ghcr.io/opentofu/opentofu:latest
    commands:
      - tofu init
      - tofu plan -out=tfplan

  - name: tofu-apply
    image: ghcr.io/opentofu/opentofu:latest
    commands:
      - tofu apply tfplan
    when:
      branch: main
      event: push

The remote state (previous step) is essential here: the CI runner does not have access to the local state on your workstation.

Separate workspaces per environment

OpenTofu offers workspaces to isolate multiple states in the same backend: tofu workspace new staging creates an isolated space, tofu workspace select production switches to production. This is lighter than duplicating .tf directories. In practice, one workspace per client or environment (staging, prod) prevents a tofu destroy in staging from touching production. Name your resources with ${terraform.workspace} to keep them distinct in the state.

Ansible and OpenTofu: the boundary between the two

The question comes up often: Ansible already does the job, why add a tool? The boundary is clear once you draw it explicitly.

OpenTofu answers "what exists?": it creates the server, assigns an IP, sets an SSH key, records its state. If you remove it from your .tf file and run tofu apply, the server disappears — OpenTofu owns the lifecycle.

Ansible answers "what state is what exists in?": it installs Docker, configures Nginx, drops an .env file, restarts a service. If the server is already there, Ansible makes it what you ask — but if it is not there, Ansible cannot create it.

The natural flow for an agency: OpenTofu provisions the VPS and exposes its IP as output, an Ansible playbook consumes that output via a dynamic inventory, configures the server and deploys applications. The article Ansible: automate the configuration of your VPS servers covers the configuration side in detail.

Troubleshooting: common errors

Three situations come up regularly when adopting OpenTofu on an existing fleet.

Common errors and their fix

  • Error acquiring the state lock — a crash during apply leaves a .terraform.tfstate.lock.info file on the backend. OpenTofu refuses to proceed while the lock exists. After verifying no other apply is running, remove the lock with tofu force-unlock <LOCK_ID> (the ID appears in the error message). On an S3/MinIO backend, the file is visible in the bucket.
  • tofu plan shows unexpected destroy + create — some resource attributes force a recreation (force new resource) when changed: server name, OS image type, region. If you modify one of these attributes, OpenTofu cannot do an in-place update — it destroys and recreates. Read the plan carefully before applying and use tofu plan -target=resource.name to narrow the scope.
  • Lost or desynchronized state — if the state file is lost and the servers still exist, tofu state list lists what OpenTofu believes is provisioned, and tofu import <resource.type.name> <external-id> imports an existing resource into the state without recreating it. This is the recovery tool when reality and state have diverged.
  • Provider not found after tofu init — verify that the provider source is exact (e.g. kreuzwerker/docker not docker/docker) and that you have internet access from the machine running tofu init. In an air-gapped environment, pre-download providers and use plugin_cache_dir.
  • Error: No valid credential sources found — OpenTofu cannot find credentials for the provider. Check the environment variables expected by the provider (often TF_VAR_api_token or a provider-specific credentials file) and that terraform.tfvars is being read (it must be in the same directory as main.tf).

Your infrastructure becomes versioned code

OpenTofu does not replace SSH, it makes SSH exceptional. The daily flow becomes: modify a .tf file, run tofu plan to review the diff, validate, run tofu apply. Servers you no longer need to manage are destroyed by tofu destroy and disappear from billing.

For an agency managing more than a dozen clients, this is the lever that turns infrastructure management from a memory exercise into a code discipline. A ServOrbit Cloud VPS with root access, dedicated IPv4 and choice of OS is the base unit that your .tf files provision and destroy on demand. Your infrastructure becomes versioned code.

A VPS Cloud ready for OpenTofu

Root access, dedicated IPv4 and choice of OS: a ServOrbit Cloud VPS is the base unit that your `.tf` files provision and destroy on demand.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.