VPS Automation with Terraform 2026: Infrastructure as Code Guide
Complete guide to automating VPS provisioning with Terraform. Provider setup for Hetzner, DigitalOcean, Vultr, and Linode, remote state, modules, and CI/CD workflows for production infrastructure.
VPS Automation with Terraform: Infrastructure as Code in 2026
Clicking through a control panel to spin up a VPS works once. It stops working when you need ten identical servers, a reproducible staging environment, or a way to rebuild your infrastructure after a disaster. Terraform turns server provisioning into version-controlled code — write a few lines of HCL, run terraform apply, and your VPS exists. Tear it down, change a region, scale up replicas, all from the same files.
This guide covers everything from your first main.tf to multi-provider modules, remote state, and CI/CD pipelines that deploy infrastructure on every git push.
Why Terraform for VPS?
Why Terraform for VPS?
| Approach | Reproducible | Version Controlled | Speed | Learning Curve |
|---|---|---|---|---|
| Terraform | Yes | Yes | Fast | Medium |
| Control panel clicks | No | No | Slow | None |
| Bash + curl scripts | Partial | Yes | Medium | Low |
| Ansible (provisioning) | Yes | Yes | Slower | Medium |
| Pulumi (Python/TS) | Yes | Yes | Fast | Medium |
When Terraform is the right tool:
- You manage more than 3-5 VPS instances
- You need staging environments that mirror production
- Multiple people touch the same infrastructure
- You want disaster recovery that doesn’t depend on a runbook
- You’re using more than one VPS provider
When to skip Terraform:
- You have one VPS and it never changes
- You’re learning Linux basics and shouldn’t add abstraction yet
- The provider doesn’t have a Terraform provider (rare — most do)
Best VPS Providers for Terraform
Terraform support comes down to two things: an official provider with active maintenance, and an API that exposes the features you actually need (networking, snapshots, firewalls, load balancers).
| Provider | Official Provider | Quality | Free Tier | Notes |
|---|---|---|---|---|
| Hetzner | hetznercloud/hcloud | Excellent | None | Best price-to-feature ratio |
| DigitalOcean | digitalocean/digitalocean | Excellent | $200 credit | Most mature ecosystem |
| Vultr | vultr/vultr | Good | $100 credit | 25 regions |
| Linode | linode/linode | Excellent | $100 credit | Akamai-owned, stable API |
| OVH | ovh/ovh | Decent | None | Complex auth flow |
| Hostinger | Community | Limited | None | Use API directly or wrapper |
| Contabo | Community | Limited | None | API exists but no official provider |
Pick Hetzner Cloud if you want the smoothest Terraform experience at the lowest cost. The provider is well-documented, the API is stable, and a CX22 (2 vCPU, 4GB RAM) costs €3.79/mo.
Installing Terraform
Pick one method — they’re all equivalent.
macOS
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
Linux
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
terraform version
# Terraform v1.7.5 or later
OpenTofu Alternative
After HashiCorp’s 2023 license change, OpenTofu forked Terraform under an open-source license. It’s a drop-in replacement — same HCL, same providers, same commands (tofu instead of terraform). For new projects, OpenTofu is worth considering. Everything in this guide works on both.
Your First Terraform VPS (Hetzner)
We’ll provision a single Hetzner VPS with a firewall, an SSH key, and a fixed IP. Total cost: about €4/month.
Step 1: Get an API Token
Log in to the Hetzner Cloud console, create a project, then Security → API Tokens → Generate API Token with read/write access. Save it — you can’t view it again.
Step 2: Create the Project
mkdir terraform-hetzner && cd terraform-hetzner
touch main.tf variables.tf outputs.tf terraform.tfvars
Step 3: Configure the Provider
main.tf:
terraform {
required_version = ">= 1.5"
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "~> 1.45"
}
}
}
provider "hcloud" {
token = var.hcloud_token
}
variables.tf:
variable "hcloud_token" {
description = "Hetzner Cloud API token"
type = string
sensitive = true
}
variable "ssh_public_key" {
description = "Path to public SSH key"
type = string
default = "~/.ssh/id_ed25519.pub"
}
variable "server_location" {
description = "Hetzner datacenter location"
type = string
default = "fsn1" # Falkenstein, Germany
}
terraform.tfvars (gitignore this!):
hcloud_token = "your_api_token_here"
Step 4: Define the Infrastructure
Add to main.tf:
resource "hcloud_ssh_key" "default" {
name = "terraform-key"
public_key = file(var.ssh_public_key)
}
resource "hcloud_firewall" "web" {
name = "web-firewall"
rule {
direction = "in"
protocol = "tcp"
port = "22"
source_ips = ["0.0.0.0/0", "::/0"]
}
rule {
direction = "in"
protocol = "tcp"
port = "80"
source_ips = ["0.0.0.0/0", "::/0"]
}
rule {
direction = "in"
protocol = "tcp"
port = "443"
source_ips = ["0.0.0.0/0", "::/0"]
}
}
resource "hcloud_server" "web" {
name = "web-01"
image = "ubuntu-24.04"
server_type = "cx22"
location = var.server_location
ssh_keys = [hcloud_ssh_key.default.id]
firewall_ids = [hcloud_firewall.web.id]
user_data = <<-EOF
#cloud-config
package_update: true
package_upgrade: true
packages:
- nginx
- ufw
runcmd:
- systemctl enable nginx
- systemctl start nginx
EOF
labels = {
environment = "production"
managed_by = "terraform"
}
}
Step 5: Define Outputs
outputs.tf:
output "server_ip" {
description = "Public IPv4 of the server"
value = hcloud_server.web.ipv4_address
}
output "server_ipv6" {
description = "Public IPv6 of the server"
value = hcloud_server.web.ipv6_address
}
output "ssh_command" {
description = "SSH command to connect"
value = "ssh root@${hcloud_server.web.ipv4_address}"
}
Step 6: Apply
# Download the Hetzner provider
terraform init
# Preview what will happen
terraform plan
# Create the infrastructure
terraform apply
Type yes when prompted. In about 15 seconds you’ll have a running VPS with nginx serving on port 80. Connect with the SSH command from the output.
Step 7: Tear Down
terraform destroy
This deletes everything — server, firewall, SSH key. You’re billed only for the minutes it ran.
Multi-Provider Examples
The HCL pattern is similar across providers. Here are the minimum viable configs for the other major VPS hosts.
DigitalOcean
terraform {
required_providers {
digitalocean = {
source = "digitalocean/digitalocean"
version = "~> 2.34"
}
}
}
provider "digitalocean" {
token = var.do_token
}
resource "digitalocean_droplet" "web" {
name = "web-01"
image = "ubuntu-24-04-x64"
region = "nyc3"
size = "s-2vcpu-4gb"
ssh_keys = [digitalocean_ssh_key.default.fingerprint]
user_data = file("${path.module}/cloud-init.yml")
tags = ["production", "web"]
}
resource "digitalocean_ssh_key" "default" {
name = "terraform-key"
public_key = file("~/.ssh/id_ed25519.pub")
}
resource "digitalocean_firewall" "web" {
name = "web-firewall"
droplet_ids = [digitalocean_droplet.web.id]
inbound_rule {
protocol = "tcp"
port_range = "22"
source_addresses = ["0.0.0.0/0", "::/0"]
}
inbound_rule {
protocol = "tcp"
port_range = "80"
source_addresses = ["0.0.0.0/0", "::/0"]
}
}
Vultr
terraform {
required_providers {
vultr = {
source = "vultr/vultr"
version = "~> 2.21"
}
}
}
provider "vultr" {
api_key = var.vultr_api_key
}
resource "vultr_ssh_key" "default" {
name = "terraform-key"
ssh_key = file("~/.ssh/id_ed25519.pub")
}
resource "vultr_instance" "web" {
plan = "vc2-2c-4gb"
region = "ewr"
os_id = 2284 # Ubuntu 24.04
label = "web-01"
hostname = "web-01"
ssh_key_ids = [vultr_ssh_key.default.id]
tags = ["production"]
}
Linode (Akamai)
terraform {
required_providers {
linode = {
source = "linode/linode"
version = "~> 2.16"
}
}
}
provider "linode" {
token = var.linode_token
}
resource "linode_instance" "web" {
label = "web-01"
image = "linode/ubuntu24.04"
region = "us-east"
type = "g6-standard-2"
authorized_keys = [chomp(file("~/.ssh/id_ed25519.pub"))]
tags = ["production"]
}
Hostinger (Community Provider)
Hostinger doesn’t ship an official Terraform provider, but the hostinger/hostinger community provider exposes the VPS API:
terraform {
required_providers {
hostinger = {
source = "hostinger/hostinger"
version = "~> 0.1"
}
}
}
provider "hostinger" {
api_token = var.hostinger_token
}
resource "hostinger_vps" "web" {
hostname = "web-01"
template_id = "ubuntu-24-04"
data_center = "fra"
plan = "kvm-2"
password = var.root_password
}
For full Hostinger VPS provisioning, many users still combine the Hostinger API with Terraform http data sources or null_resource blocks — the community provider has gaps.
State Management
Terraform tracks what it created in a state file (terraform.tfstate). By default this lives on your local disk, which fails the moment two people work on the same project, or you switch machines, or you lose your laptop.
The Local State Problem
You: terraform apply # state on your laptop
Coworker: terraform apply # state on their laptop
Result: duplicate servers, drift, sadness
State must live somewhere shared, with locking so two applys can’t run simultaneously.
Remote State Options
| Backend | Cost | Locking | Setup Complexity |
|---|---|---|---|
| Terraform Cloud (HCP) | Free up to 500 resources | Yes | Easy |
| AWS S3 + DynamoDB | ~$1/mo | Yes | Medium |
| Hetzner Object Storage | €1.19/mo | Yes (via lockfile) | Medium |
| GitLab managed state | Free with GitLab | Yes | Easy |
| Self-hosted (Consul/Postgres) | Server cost | Yes | High |
Terraform Cloud (Easiest)
main.tf:
terraform {
cloud {
organization = "your-org"
workspaces {
name = "production"
}
}
}
Then terraform login and terraform init — state lives in HCP, runs happen on their infra, and you get a web UI for free.
S3 Backend (Most Common)
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "production/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}
Create the bucket and DynamoDB table once (manually or with a bootstrap Terraform config you keep separate). Every other project uses this same backend with a different key.
Object Storage on Hetzner / DigitalOcean
Both providers offer S3-compatible object storage. Use the s3 backend with custom endpoints:
terraform {
backend "s3" {
bucket = "terraform-state"
key = "production/terraform.tfstate"
region = "fsn1"
endpoints = {
s3 = "https://fsn1.your-objectstorage.com"
}
skip_credentials_validation = true
skip_region_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true
}
}
This keeps state inside your existing provider — no AWS dependency.
Modules: Don’t Repeat Yourself
After your third copy-pasted VPS resource block, extract a module. Modules are reusable parameterized templates.
Structure
.
├── main.tf
├── variables.tf
├── outputs.tf
└── modules/
└── web-server/
├── main.tf
├── variables.tf
└── outputs.tf
modules/web-server/main.tf
variable "name" { type = string }
variable "server_type" { type = string, default = "cx22" }
variable "location" { type = string, default = "fsn1" }
variable "ssh_key_ids" { type = list(string) }
variable "firewall_ids" { type = list(string), default = [] }
resource "hcloud_server" "this" {
name = var.name
image = "ubuntu-24.04"
server_type = var.server_type
location = var.location
ssh_keys = var.ssh_key_ids
firewall_ids = var.firewall_ids
labels = {
managed_by = "terraform"
module = "web-server"
}
}
output "id" { value = hcloud_server.this.id }
output "ipv4" { value = hcloud_server.this.ipv4_address }
Using the Module
main.tf:
module "app_server" {
source = "./modules/web-server"
name = "app-01"
server_type = "cx32"
ssh_key_ids = [hcloud_ssh_key.default.id]
firewall_ids = [hcloud_firewall.web.id]
}
module "worker_servers" {
source = "./modules/web-server"
count = 3
name = "worker-${count.index + 1}"
server_type = "cx22"
ssh_key_ids = [hcloud_ssh_key.default.id]
firewall_ids = [hcloud_firewall.web.id]
}
output "app_ip" { value = module.app_server.ipv4 }
output "worker_ips" { value = module.worker_servers[*].ipv4 }
One call, four servers. Change count = 3 to count = 10, run terraform apply, get six more workers.
Public Modules
The Terraform Registry hosts community modules. For VPS providers specifically, official module quality varies — read the source before trusting one.
Combining Terraform with Ansible
Terraform creates the server. Ansible configures what runs on it. The handoff happens via Terraform outputs that feed into an Ansible inventory.
Approach 1: Inventory File
resource "local_file" "ansible_inventory" {
content = templatefile("${path.module}/inventory.tpl", {
web_servers = hcloud_server.web[*].ipv4_address
worker_servers = module.worker_servers[*].ipv4
})
filename = "${path.module}/../ansible/inventory.ini"
}
inventory.tpl:
[web]
%{ for ip in web_servers ~}
${ip}
%{ endfor ~}
[workers]
%{ for ip in worker_servers ~}
${ip}
%{ endfor ~}
[all:vars]
ansible_user=root
Then:
terraform apply
cd ../ansible && ansible-playbook -i inventory.ini site.yml
Approach 2: Provisioners (Avoid When Possible)
Terraform’s remote-exec and local-exec provisioners can run commands directly:
resource "hcloud_server" "web" {
# ...
provisioner "local-exec" {
command = "ansible-playbook -i '${self.ipv4_address},' site.yml"
}
}
This works but couples provisioning and configuration. If Ansible fails, Terraform marks the resource as tainted. Most teams prefer separate steps. For more on Ansible, see our Best VPS for Ansible guide.
Approach 3: Cloud-Init Only
For simple setups, skip Ansible entirely and put everything in user_data:
user_data = templatefile("${path.module}/cloud-init.yml", {
hostname = "web-01"
ssh_keys = [file("~/.ssh/id_ed25519.pub")]
})
Cloud-init handles package install, user creation, SSH keys, file writes, and systemd units. Fine for small static fleets — gets unwieldy past 50 lines.
CI/CD: Terraform in GitHub Actions
Don’t run terraform apply from your laptop in production. Run it from a pipeline triggered by git.
.github/workflows/terraform.yml:
name: Terraform
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
pull-requests: write
jobs:
terraform:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./infra
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.7.5
- name: Terraform Format
run: terraform fmt -check
- name: Terraform Init
run: terraform init
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
if: github.event_name == 'pull_request'
run: terraform plan -no-color -out=tfplan
env:
TF_VAR_hcloud_token: ${{ secrets.HCLOUD_TOKEN }}
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve
env:
TF_VAR_hcloud_token: ${{ secrets.HCLOUD_TOKEN }}
PRs show a plan in the comments. Merges to main trigger an apply. State lives in S3 with DynamoDB locking, so concurrent runs are impossible.
GitLab CI Variant
stages:
- validate
- plan
- apply
variables:
TF_ROOT: ${CI_PROJECT_DIR}/infra
before_script:
- cd ${TF_ROOT}
- terraform init
validate:
stage: validate
script:
- terraform fmt -check
- terraform validate
plan:
stage: plan
script:
- terraform plan -out=plan.tfplan
artifacts:
paths:
- ${TF_ROOT}/plan.tfplan
apply:
stage: apply
script:
- terraform apply -auto-approve plan.tfplan
only:
- main
when: manual
GitLab supports managed Terraform state natively — no S3 setup required.
Workspaces vs Directories for Environments
Terraform has two patterns for managing dev/staging/prod:
Workspaces (Lighter)
terraform workspace new staging
terraform workspace new production
terraform workspace select staging
terraform apply
State is separated by workspace, code is shared. Use terraform.workspace in HCL:
locals {
server_count = terraform.workspace == "production" ? 5 : 1
}
Downside: every workspace runs identical code. Hard to diverge — and you will eventually.
Directories (Better for Real Projects)
infra/
├── modules/
│ └── web-stack/
├── staging/
│ ├── main.tf
│ └── terraform.tfvars
└── production/
├── main.tf
└── terraform.tfvars
Each environment has its own backend config, its own state, its own deploy cadence. The shared logic lives in modules/. This is what most production teams converge on.
Cost Estimation Before Apply
terraform plan shows what will change, not what it will cost. For that, use Infracost:
# Install
brew install infracost
# Auth (free tier available)
infracost auth login
# Run on a plan
terraform plan -out=tfplan
infracost breakdown --path=tfplan
Output looks like:
Project: terraform-hetzner
Name Monthly Qty Unit Monthly Cost
hcloud_server.web
└─ Instance usage (CX22) 720 hours €3.79
OVERALL TOTAL €3.79
Add Infracost to CI to comment cost diffs on every PR — surprising spend caught at review time saves more than the tool costs.
Sensitive Values
Never commit terraform.tfvars if it contains tokens. Standard .gitignore:
*.tfvars
*.tfvars.json
.terraform/
.terraform.lock.hcl
terraform.tfstate
terraform.tfstate.backup
crash.log
override.tf
override.tf.json
Keep .terraform.lock.hcl in git — that pins provider versions across the team.
Env Vars Pattern
export TF_VAR_hcloud_token="..."
terraform apply
Any TF_VAR_<name> env var auto-populates the matching variable "<name>". Works the same in CI.
Secret Stores
For production, fetch secrets at plan time from Vault, AWS Secrets Manager, or 1Password CLI:
data "external" "vault_token" {
program = ["sh", "-c", "echo '{\"token\":\"'$(vault kv get -field=token secret/hcloud)'\"}'"]
}
provider "hcloud" {
token = data.external.vault_token.result.token
}
This keeps tokens out of env vars, tfvars, and CI secret stores entirely.
Common Issues
”Error: Resource already exists”
You created the resource manually, then tried to manage it with Terraform. Import it:
terraform import hcloud_server.web 12345678
Replace 12345678 with the server ID from the Hetzner dashboard.
Drift
Someone changed a resource in the control panel. terraform plan will show the diff. Fix it by either:
- Updating your
.tfto match reality, thenterraform applyto re-sync - Running
terraform applyto revert the manual change
State Locked
Error: Error acquiring the state lock
Another apply is running, or one crashed without releasing the lock. Wait, or force-unlock:
terraform force-unlock LOCK_ID
Only do this when you’re certain no other apply is in flight.
Provider Authentication
Error: Required token authentication missing
You forgot to set the env var or tfvar. Check:
echo $TF_VAR_hcloud_token
Slow Plans
If terraform plan takes minutes, your state is huge. Split it: separate stacks per environment, per service, or per region. State files over 5MB get painful.
Production Patterns
One Provider, Multiple Regions
provider "hcloud" {
alias = "eu"
token = var.hcloud_token
}
provider "hcloud" {
alias = "us"
token = var.hcloud_token
}
resource "hcloud_server" "eu_web" {
provider = hcloud.eu
location = "fsn1"
# ...
}
resource "hcloud_server" "us_web" {
provider = hcloud.us
location = "ash" # Ashburn, VA
# ...
}
Multi-Provider Fleet
Want primary on Hetzner with a DR replica on DigitalOcean? Both providers configured in the same workspace, both managed by the same apply.
resource "hcloud_server" "primary" {
# ...
}
resource "digitalocean_droplet" "replica" {
# ...
}
The state file knows about both. Terraform doesn’t care that they’re different vendors.
Snapshot Backups
resource "hcloud_server" "db" {
name = "db-01"
# ...
backups = true # Hetzner managed backups (+20%)
}
For DigitalOcean:
resource "digitalocean_droplet" "db" {
# ...
backups = true
}
Backups cost extra but enable point-in-time restore from the same Terraform definition.
Floating IPs for Failover
resource "hcloud_floating_ip" "web" {
type = "ipv4"
home_location = "fsn1"
}
resource "hcloud_floating_ip_assignment" "web" {
floating_ip_id = hcloud_floating_ip.web.id
server_id = hcloud_server.web_primary.id
}
Reassign the floating IP to a replacement server in seconds — DNS doesn’t have to change.
Terraform vs Alternatives
| Tool | Language | State | Multi-Cloud | Maturity |
|---|---|---|---|---|
| Terraform | HCL | File / Remote | Yes | Very high |
| OpenTofu | HCL | File / Remote | Yes | High (fork) |
| Pulumi | Python/TS/Go/C# | Pulumi Cloud or self-host | Yes | High |
| Crossplane | YAML (K8s CRDs) | etcd | Yes | Medium |
| Ansible | YAML | Stateless | Limited | High |
| CDK for Terraform | TS/Python | Terraform | Yes | Medium |
For VPS providers specifically, Terraform/OpenTofu remains the best choice — every provider ships an HCL provider, the ecosystem is huge, and tooling like Infracost, tflint, and Checkov target HCL directly. Pulumi is a real alternative if your team strongly prefers a general-purpose language over HCL.
Recommended Project Layout
infra/
├── README.md
├── .gitignore
├── modules/
│ ├── web-server/
│ ├── database/
│ └── load-balancer/
├── environments/
│ ├── staging/
│ │ ├── main.tf
│ │ ├── backend.tf
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ └── production/
│ ├── main.tf
│ ├── backend.tf
│ ├── variables.tf
│ └── terraform.tfvars
└── .github/
└── workflows/
└── terraform.yml
Keep modules generic, environments specific. Variables flow inward — tfvars → environment main.tf → module inputs.
FAQ
Is Terraform free?
The CLI is free and open source (now under BSL after the 2023 license change). OpenTofu is the MPL-licensed community fork. Terraform Cloud has a free tier; paid plans start at $20/user/month.
Can I manage existing VPS with Terraform?
Yes — use terraform import to bring a manually-created resource under Terraform management. You’ll still need to write the matching .tf block first.
Should I use Terraform for one VPS?
Probably not. The overhead pays off at 3+ servers, multiple environments, or team workflows. For a single side project, a cloud-init.yml and the provider’s web UI is faster.
What about Kubernetes?
Use Terraform to provision the VPS nodes, then a separate tool (kubeadm, k3s, Talos, Helm) to install Kubernetes and deploy workloads. See our Kubernetes on VPS guide for the runtime layer.
Does Terraform work with cheap VPS providers like RackNerd?
RackNerd doesn’t expose a public API, so neither Terraform nor the alternatives work directly. For unmanaged budget VPS without APIs, you’re back to manual provisioning or SSH-based config tools.
How do I rotate API tokens?
Update the token in your secret store (env var, Vault, etc.), rerun terraform plan to confirm nothing changes, then revoke the old token in the provider’s dashboard.
Can I deploy applications with Terraform?
You can, but it’s not ideal. Terraform is built for infrastructure. For app deployment, use a tool built for it: a CI/CD pipeline, Coolify, Dokploy, or Kubernetes.
Next Steps
- Pick one provider and write your first
main.tf— Hetzner Cloud is the cheapest place to learn - Move state to remote — even for solo projects, future-you will thank you
- Extract a module the second time you copy-paste a resource block
- Wire up CI — manual
applyfrom a laptop doesn’t scale past one person - Add Infracost to PR reviews so cost surprises stop happening
Infrastructure as code isn’t just for cloud giants. With Hetzner at €3.79/mo or Hostinger at $5.99/mo, you can run a fully Terraform-managed multi-server fleet for less than a streaming subscription. The investment in Terraform skills pays off the first time you rebuild your staging environment in five minutes instead of five hours.
If you’re combining provisioning with config management, pair Terraform with Ansible on a control node — see our Best VPS for Ansible guide for that setup. And once your VPS fleet is humming, consider hardening it with our VPS hardening guide and adding a WireGuard VPN to lock down management traffic.
Ready to get started?
Get the best VPS hosting deal today. Hostinger offers 4GB RAM VPS starting at just $4.99/mo.
Get Hostinger VPS — $4.99/mo// up to 75% off + free domain included
// related topics
// related guides
$1 VPS Hosting 2026: Cheapest VPS Servers Starting at $1/Month
Looking for $1 VPS hosting? Compare the cheapest VPS providers starting from $1-3/month. Real specs, no hidden fees, honest reviews of budget VPS options.
tutorialCaddy Reverse Proxy Guide 2026: Automatic HTTPS Made Easy
Set up Caddy as a reverse proxy with automatic HTTPS, zero-config SSL, and simple Caddyfile syntax. Complete VPS deployment guide.
tutorialCloudflare Tunnel VPS Guide 2026: Expose Services Without Opening Ports
Set up Cloudflare Tunnel on your VPS to expose web apps securely without opening ports or revealing your server IP. Complete guide with Docker and DNS config.
tutorialCoolify VPS Setup Guide 2026: Self-Hosted Vercel Alternative
Deploy Coolify on your VPS for a self-hosted Vercel/Netlify experience. Complete setup guide with Docker, SSL, and app deployments.
Andrius Putna
I am Andrius Putna. Geek. Since early 2000 in love tinkering with web technologies. Now AI. Bridging business and technology to drive meaningful impact. Combining expertise in customer experience, technology, and business strategy to deliver valuable insights. Father, open-source contributor, investor, 2xIronman, MBA graduate.
// last updated: May 12, 2026. Disclosure: This article may contain affiliate links.