Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Terraform for VPS Provisioning Beginners Guide

By Kokil Thapa | Last reviewed: September 2026

You need a repeatable way to spin up Ubuntu VPS instances without clicking through five provider dashboards every time. This Terraform for VPS Provisioning Beginners Guide shows how to define servers, firewalls, and SSH access in version-controlled files. If you already ship Laravel or WordPress on bare VPS hosts, infrastructure as code with Terraform turns that manual setup into something you can replay in minutes. The examples below target PHP application stacks on Ubuntu 24.04, but the pattern works for any Linux workload.

What is Terraform for VPS provisioning and why use it?

Terraform is HashiCorp's open-source tool for declaring cloud resources in HCL files. You describe what you want—a VPS, a floating IP, a firewall rule—and Terraform talks to the provider API to create or update it. The same files work on your laptop, in CI, and on a teammate's machine.

On real client projects I maintain several sister sites on shared EC2 infrastructure with Deployer 7 and GitLab CI. Before Terraform, each new staging box meant manual clicks in the provider panel. After codifying the VPS layer, spinning up a mirror environment takes one pipeline job instead of an afternoon.

Terraform handles provisioning: creating the virtual machine, attaching storage, opening ports 22 and 443. It does not install PHP-FPM, Nginx, or MySQL. That work belongs to configuration management. The split is intentional and matches how most production teams operate. See the dedicated comparison in our Terraform vs Ansible guide for the full picture.

Terraform VPS Provisioning FlowHCL Filesmain.tf vars.tfTerraforminit plan applyProvider APIDO Hetzner AWSLive VPSUbuntu 24.04Post-provision: Ansible or shellPHP Nginx MySQL Deployer GitLab CIRemote State BackendS3 Terraform Cloud or local with caution
Terraform for VPS provisioning creates the server; configuration tools install your application stack afterward.

How do you install Terraform and pick a VPS provider?

Start with Terraform CLI 1.9 or later and a provider account. DigitalOcean and Hetzner Cloud are popular for budget VPS work. AWS EC2 fits when you already run other services in the same region. Pick one provider for your first project; multi-cloud can wait.

Install Terraform on Ubuntu or macOS

On Ubuntu 24.04, use HashiCorp's official apt repository:

wget -O- 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 $(lsb_release -cs) main" | \
sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform

Verify with terraform version. You should see Terraform v1.9+ and a provider list after your first init.

Generate provider API tokens

Each cloud needs a token stored outside your repo:

  • DigitalOcean: Personal Access Token with read/write scope.
  • Hetzner Cloud: Project API token from the Hetzner Cloud Console.
  • AWS: IAM user with EC2, VPC, and security-group permissions.

Export tokens as environment variables. Never hard-code them in .tf files or commit terraform.tfvars with secrets to Git.

ProviderTypical 2 vCPU / 4 GB VPSBest forTerraform provider
DigitalOcean~USD 24/month (~Rs 3,200)Simple API, good docs, Nepal-friendly billingdigitalocean/digitalocean
Hetzner Cloud~USD 8/month (~Rs 1,100)Lowest cost EU/US regionshetznercloud/hcloud
AWS EC2~USD 30+/month (~Rs 4,000)Existing AWS workloads, IAM integrationhashicorp/aws
Linode (Akamai)~USD 24/month (~Rs 3,200)Predictable pricing, solid supportlinode/linode

For Nepal-based clients on tight budgets, Hetzner or DigitalOcean often beat local dedicated hardware on total cost once you factor in power and backup. Our domain and hosting service covers the DNS side once your VPS IP is ready.

How do you write your first Terraform VPS configuration?

Create a project folder with a clear layout. Keep provider config, server definition, and variables in separate files from day one. You will thank yourself when the project grows.

terraform-vps/
├── main.tf          # provider + droplet/resource
├── variables.tf     # input declarations
├── terraform.tfvars # non-secret values (size, region)
├── outputs.tf       # IP address, hostname
├── versions.tf      # terraform + provider pins
└── .gitignore       # .terraform/, *.tfstate, *.tfvars with secrets

DigitalOcean example: Ubuntu VPS with firewall

Pin provider versions in versions.tf. This prevents surprise breaking changes during terraform init:

terraform {
  required_version = ">= 1.9.0"
  required_providers {
    digitalocean = {
      source  = "digitalocean/digitalocean"
      version = "~> 2.40"
    }
  }
}

The main resource block in main.tf:

provider "digitalocean" {
  token = var.do_token
}

resource "digitalocean_ssh_key" "deploy" {
  name       = "deploy-key"
  public_key = file("~/.ssh/id_ed25519.pub")
}

resource "digitalocean_droplet" "app" {
  name     = "laravel-staging"
  region   = var.region
  size     = var.droplet_size
  image    = "ubuntu-24-04-x64"
  ssh_keys = [digitalocean_ssh_key.deploy.fingerprint]

  tags = ["staging", "terraform-managed"]
}

resource "digitalocean_firewall" "app" {
  name = "app-firewall"
  droplet_ids = [digitalocean_droplet.app.id]

  inbound_rule {
    protocol         = "tcp"
    port_range       = "22"
    source_addresses = ["YOUR_OFFICE_IP/32"]
  }
  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"]
  }
}

Declare variables in variables.tf. Our Terraform variables and outputs guide covers locals, defaults, and sensitive flags in depth.

variable "do_token" {
  type      = string
  sensitive = true
}

variable "region" {
  type    = string
  default = "sgp1"
}

variable "droplet_size" {
  type    = string
  default = "s-2vcpu-4gb"
}

Output the IP so your CI pipeline or Ansible inventory can consume it:

output "droplet_ip" {
  value = digitalocean_droplet.app.ipv4_address
}
Terraform Core Workflowterraform initDownload providersterraform planPreview changesterraform applyCreate VPS + FWStateUpdatedState file maps HCL to real resource IDsdroplet id 12345678 maps to laravel-stagingNever edit state by hand unless you know whyterraform destroy removes all managed resourcesUse only on disposable staging boxes
The Terraform init, plan, apply cycle is the daily loop for VPS provisioning beginners.

How do you run Terraform safely with state and secrets?

State is Terraform's memory of what it created. Lose it and Terraform forgets your droplet exists. Commit it to a public repo and you expose internal IP addresses and resource IDs. Treat state like a database backup.

Local state for learning only

Your first terraform apply writes terraform.tfstate locally. That is fine for a throwaway lab box. Delete the VPS with terraform destroy when you finish experimenting.

Remote backend for anything shared

Once two people touch the same stack, move state to a remote backend. Options include AWS S3 plus DynamoDB locking, Terraform Cloud free tier, or a self-hosted OpenTofu-compatible backend. Our guide on managing Terraform state safely walks through S3 encryption and state locking step by step.

Pass secrets via environment variables, not files in Git:

export TF_VAR_do_token="dop_v1_xxxxxxxx"
terraform plan -out=plan.tfplan
terraform apply plan.tfplan

The -out flag saves the approved plan. CI pipelines apply exactly what was reviewed, with no surprise drift. Use a strong password generator for database credentials you inject later via Ansible, not via Terraform outputs logged in plain text.

Workspaces and environments

Separate staging from production with Terraform workspaces or separate root modules. I have seen teams reuse one state file for both and accidentally destroy production during a staging experiment. Workspaces are cheap insurance. Read Terraform workspaces and environments before you add a second VPS.

How do you connect Terraform to your application deployment pipeline?

Provisioning stops at a booted Ubuntu box with SSH open. Your Laravel, Symfony, or WordPress stack still needs Nginx, PHP-FPM 8.3 or 8.5, MySQL 8.4, and a Deployer or GitLab CI hook. Chain the tools instead of forcing Terraform to run shell provisioners for everything.

Step-by-step: from apply to running app

  1. Run terraform apply and capture the output IP.
  2. Point your domain A record at that IP. Use your registrar or Cloudflare.
  3. Run an Ansible playbook or follow Laravel on Ubuntu VPS with Nginx.
  4. Configure GitLab CI or GitHub Actions to deploy on push, as in Deploy a Laravel app with GitLab CI/CD to a VPS.
  5. Enable nightly database dumps and monitor disk space. Small VPS instances fill up fast with log files.

I've used this exact split on sister sites sharing Deployer 7 and GitLab CI on shared EC2. Terraform creates the instance and security group. Ansible installs the LAMP or LEMP stack. Deployer handles zero-downtime releases with symlinked directories.

For WordPress migrations off managed hosting, provision the VPS first, then follow our WordPress migration to VPS guide. Symfony teams can mirror the same flow using the Symfony on Ubuntu VPS tutorial.

Manual vs Terraform VPS SetupManual Dashboard Clicks45 min per serverNo audit trailConfig drift over timeHard to reproduceForgotten firewall rulesSingle person dependencyCommon on legacy projectsTerraform IaC5 min apply after first writeGit history = audit logIdentical staging and prodPeer review via pull requestFirewall codified in HCLTeam can replay anytimeUsed on production client stacksSame app deploy path either way: Deployer 7 + GitLab CI
Terraform for VPS provisioning eliminates repeat manual work while keeping your existing Deployer or CI deploy flow intact.

What mistakes do Terraform beginners make on VPS projects?

Most failures I troubleshoot are not Terraform syntax errors. They are operational gaps: missing lifecycle rules, open SSH to the world, or applying production changes without a plan review.

Common gotchas and fixes

  • Hard-coded IPs in firewall rules. Use variables and update them when your office IP changes.
  • No prevent_destroy on production. Add a lifecycle block on critical droplets. One mistyped destroy ends badly.
  • Provisioner abuse. Remote-exec provisioners are fragile. Use Ansible playbooks for PHP server provisioning instead.
  • Unpinned provider versions. A major provider upgrade during init can break your plan. Pin versions in versions.tf.
  • Skipping terraform fmt and validate. Add both to CI before apply, as shown in Terraform CI/CD with GitHub Actions.

Extract repeated patterns into modules once you manage more than two similar VPS stacks. The Terraform modules guide shows how to parameterise region, size, and tags without copy-pasting blocks.

Validate JSON outputs from your CI webhook handlers with a JSON formatter during pipeline debugging. Misformatted payload files cause more late-night pages than Terraform itself.

Real Stack: Legal-Tech VPS ExampleTerraform ApplyEC2 + SG + EIPUbuntu 24.04PHP 8.4 FPM NginxLaravel PortalNotary booking + docsGitLab CI: lint then Deployer 7 deployZero-downtime symlink releases on shared EC2MySQL 8.4 + nightly dumpCron on persistent volumeLet's Encrypt TLSCertbot auto-renew cron
Production legal-tech portals like Notary Kathmandu use Terraform-layer VPS provisioning with Deployer 7 and GitLab CI on shared infrastructure.

Sister sites such as Notary Kathmandu and Translation Nepal share the same deploy pipeline pattern. Terraform is not part of every small brochure site, but any portal handling bookings, documents, and payments benefits from reproducible infrastructure. See more examples on the portfolio page.

Official references worth bookmarking: the HashiCorp Terraform documentation and the DigitalOcean Terraform provider docs. Both stay current with provider schema changes.

If Terraform licensing concerns your team, evaluate OpenTofu as a drop-in fork. Functionality for basic VPS provisioning remains equivalent for most beginner stacks.

Ongoing server care—PHP version upgrades, opcache reloads after deploy, disk alerts—still needs a human or a managed service. Our Linux system administration service and support and maintenance plans cover that layer after Terraform hands you the keys.

Key Takeaways

  • Define your VPS, SSH key, and firewall in HCL; run init, plan, then apply.
  • Store state remotely with locking once anyone besides you touches the stack.
  • Never commit API tokens—use TF_VAR environment variables or a secret manager.
  • Terraform provisions the box; Ansible or shell scripts install PHP, Nginx, and MySQL.
  • Pin provider versions and add terraform fmt plus validate to CI before apply.
  • Chain Terraform output IPs into Deployer or GitLab CI for the same deploy path you use today.

People Also Ask

Can Terraform install PHP and Nginx on my VPS?

Technically yes, via remote-exec or cloud-init user data. In practice, Ansible or a well-tested shell script is more maintainable. Keep Terraform focused on the provider resources—droplet, firewall, volume, DNS record—and let configuration management handle packages and services.

Is Terraform free for personal VPS projects?

The Terraform CLI is free and open source. You pay only for the VPS itself—often USD 8–30 per month (~Rs 1,100–4,000). Terraform Cloud offers a free tier for remote state and CI integration if you outgrow local state files.

What is the difference between Terraform and Docker for VPS hosting?

Terraform creates the virtual machine and network rules on a cloud provider. Docker runs application containers inside an already-provisioned host. Many teams use both: Terraform for the VPS, Docker for app isolation, Nginx as reverse proxy on the host.

Should beginners start with Terraform or Ansible?

Start with Terraform if you need new VPS instances on demand. Start with Ansible if you already have a server and need to install software. Most production PHP stacks use Terraform first, then Ansible, then Deployer or CI for application code.

Ship reproducible VPS infrastructure this week

You now have a complete Terraform for VPS Provisioning Beginners Guide path: pick a provider, write HCL, protect state, and hand off to your existing deploy toolchain. Start with one staging droplet, destroy it cleanly, then codify production once the plan output looks right.

Need help wiring Terraform into a Laravel legal portal, WooCommerce store, or multi-site Deployer pipeline? Contact us or explore enterprise application development to scope the full stack—from VPS to production deploy.

Frequently Asked Questions

HashiCorp’s open-source tool for declaring cloud resources in HCL files. You describe a VPS, floating IP, or firewall rule; Terraform calls the provider API to create or update them. Same files run on your laptop, in CI, or on a teammate’s machine.

The Terraform CLI is free and open source. You pay only for the VPS—often USD 8–30 per month (~Rs 1,100–4,000). Terraform Cloud’s free tier covers remote state and CI if local files are not enough.

Technically yes via remote-exec or cloud-init, but Ansible or shell scripts are more maintainable. Keep Terraform on provider resources—droplet, firewall, volume—and use configuration management for packages and services.

Start with one provider; multi-cloud can wait. DigitalOcean and Hetzner Cloud suit budget VPS work. AWS EC2 fits existing AWS workloads. For Nepal clients on tight budgets, Hetzner (~USD 8/month, ~Rs 1,100) or DigitalOcean (~USD 24/month, ~Rs 3,200) often beat local hardware once power and backup are counted. Linode is another predictable option at similar DigitalOcean pricing.

Use HashiCorp’s official apt repository: import the GPG key, add the HashiCorp source list, then run apt update and apt install terraform. Verify with terraform version—you should see Terraform v1.9+ and a provider list after your first terraform init. Install on macOS the same way via HashiCorp’s distribution channel before writing any .tf files.

Split concerns from day one: main.tf for provider and droplet, variables.tf for inputs, terraform.tfvars for non-secret values like size and region, outputs.tf for the IP address, versions.tf to pin Terraform and provider versions, and .gitignore excluding .terraform/, *.tfstate, and tfvars files holding secrets. This layout scales when you add firewalls, SSH keys, or a second environment.

Pin Terraform >= 1.9.0 and digitalocean provider ~> 2.40 in versions.tf. In main.tf declare the provider token via variable, upload your SSH public key, create an ubuntu-24-04-x64 droplet, and attach a firewall allowing TCP 22 from your office IP and TCP 443 from anywhere. Output droplet_ip for CI or Ansible. Declare region and droplet_size as variables with sensible defaults like sgp1 and s-2vcpu-4gb.

Run terraform init to download providers, then terraform plan to preview changes. Export secrets as environment variables—export TF_VAR_do_token—not hard-coded in .tf files. Use terraform plan -out=plan.tfplan and terraform apply plan.tfplan so CI applies exactly what was reviewed. Add terraform fmt and validate before apply in CI. For throwaway lab boxes, local state is fine; destroy with terraform destroy when done.

Local terraform.tfstate works for solo learning only. Once two people touch the same stack, move state to a remote backend with locking—AWS S3 plus DynamoDB, Terraform Cloud free tier, or a self-hosted OpenTofu-compatible backend. State is Terraform’s memory of created resources; losing it means Terraform forgets your droplet exists. Never commit state to a public repo—it exposes internal IPs and resource IDs.

Generate provider tokens outside the repo—DigitalOcean Personal Access Token, Hetzner Cloud project API token, or AWS IAM credentials with EC2 and VPC permissions. Export them as environment variables such as TF_VAR_do_token. Mark token variables sensitive = true in variables.tf. Never hard-code tokens in .tf files or commit terraform.tfvars containing secrets to Git. Inject database passwords later via Ansible, not Terraform outputs logged in plain text.

Terraform provisions infrastructure—creating the VM, storage, and opening ports 22 and 443. Ansible installs software—Nginx, PHP-FPM 8.3 or 8.5, MySQL 8.4—on an existing server. Start with Terraform if you need new VPS instances on demand; start with Ansible if the server already exists. Most production PHP stacks chain both, then Deployer or GitLab CI for application code.

Terraform creates the virtual machine and network rules on a cloud provider. Docker runs application containers inside an already-provisioned host. They solve different layers. Many teams use both: Terraform provisions the Ubuntu VPS and firewall, Docker isolates the app, and Nginx on the host acts as reverse proxy. Neither replaces the other for beginners who need a repeatable way to spin up bare VPS instances.

Terraform stops at a booted Ubuntu box with SSH open. After terraform apply, capture the output IP, point your domain A record at it, then run Ansible or follow a Laravel-on-Ubuntu-VPS guide to install Nginx and PHP-FPM. Wire GitLab CI or GitHub Actions to deploy on push. On sister sites I maintain, Terraform creates the instance and security group, Ansible installs the stack, and Deployer 7 handles zero-downtime symlinked releases—the same pattern works for WordPress migrations off managed hosting.

Restrict SSH: open TCP port 22 only from your office IP using a /32 CIDR, not 0.0.0.0/0. Open TCP 443 to 0.0.0.0/0 and ::/0 for HTTPS traffic. Allow outbound TCP 1-65535 so the server can reach package mirrors, APIs, and payment gateways. Hard-coding IPs in firewall rules is a common beginner mistake—use variables and update them when your office IP changes.

Most failures are operational, not syntax errors. Common gotchas: SSH open to the world, no prevent_destroy lifecycle on production droplets, remote-exec provisioners instead of Ansible, unpinned provider versions breaking init, one state file shared between staging and production, and applying without plan review. Separate environments with workspaces or separate root modules. Extract repeated patterns into modules once you manage more than two similar VPS stacks. Pin provider versions in versions.tf and run fmt plus validate in CI before every apply.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: