Skip to content
SHC Docs

Hostinger

Hostinger sells KVM VPS instances with a public API and an official Terraform provider. SHC runs on it as a plain compute substrate: public-IP Debian boxes, provisioned by terraform/modules/shc/hostinger and bootstrapped by terraform-provider-shc over SSH.

It is the smallest substrate SHC supports, and the page below is mostly a list of things other clouds have and this one does not. Read the cost section first — it is the reason this module ships UNVERIFIED-LIVE, which no other cloud module in the tree does.

One-line summary. Hostinger is a substrate-only, single-box-shaped cloud: no private network, no load balancer, no block volumes, no cloud firewall in terraform, no tags. Supported topologies are one node, or many nodes over the nebula WAN overlay. DNS always comes from another provider.


This is the headline, not a footnote.

Hostinger does not rent VPS capacity by the hour. Every node is a purchased subscription whose term is encoded in the plan SKU, the shortest term sold is one month, and tofu destroy cancels the subscription rather than stopping a meter. There is no proration. A ten-minute test cluster costs a full month per node.

plan SKUvCPU / RAM / diskfirst monthrenewal
hostingercom-vps-kvm1-usd-1m1 / 4 GB / 50 GB$9.99$19.49
hostingercom-vps-kvm2-usd-1m2 / 8 GB / 100 GB$13.99$24.49
hostingercom-vps-kvm4-usd-1m4 / 16 GB / 200 GB$25.99$42.99
hostingercom-vps-kvm8-usd-1m8 / 32 GB / 400 GB$50.99$73.99

(USD, read live from the catalog on 2026-07-31; first-month prices are promotional. -usd-1y and -usd-2y variants buy longer terms at a lower monthly rate. Read the current numbers yourself — see below.)

Terminal window
curl -s -H "Authorization: Bearer $HOSTINGER_API_TOKEN" \
'https://developers.hostinger.com/api/billing/v1/catalog?category=VPS' |
jq -r '.[] | .id as $p | .prices[] | [$p, .id, .price, .first_period_price] | @tsv'

Prices are in cents. Two consequences are wired into the code rather than left to discipline:

  • the module refuses to plan unless accept_monthly_billing = true;
  • the test lane refuses to run unless SHC_TEST_HOSTINGER_ACCEPT_BILLING=1, and its SHC_TEST_NODE_COUNT defaults to 1, not 3.

A leftover node here does not leak pennies per hour — it renews at a full month. That is why tests/clouds/hostinger/scripts/destroy.sh finishes by asking the API whether any VPS still carries the lane prefix, and why the lane tears down even on a failed run.

LIVE-PROVEN 2026-07-31. A 1-node cell and a 2-node nebula-overlay cell both ran SCENARIO PASS on a real account (fra, kvm1): purchase, post-install prep, shc init --nebula, cloudflare DNS + Let’s Encrypt cert, shc node join across two VPSes, mesh enrollment and bidirectional mesh-IP pings — then an API-verified teardown with the subscriptions cancelled. The module README’s banner carries the full findings list (provider bugs, WAF gotcha, FQDN hostname rule, recreate password rule).


ConcernHostinger answer
Placement axisdata_center only — no region/zone layer, and nothing about the cluster depends on nodes sharing one. Short names: fra (Frankfurt), bos / bos2 (Boston), fast (Manchester), mum2 (Mumbai), dci (Jakarta), kul (Kuala Lumpur), cam (Campinas).
Substrate authA single API token (HOSTINGER_API_TOKEN), Bearer-authenticated.
Boot imagetemplate_name resolved by data "hostinger_vps_templates", EXACT match. Default "Debian 13" (id 1188; "Debian 12" is 1031). The catalog also carries ~90 "<OS> with <app>" variants — never select one by accident. Every KVM plan is x86/amd64, which the shc .deb requires.
SSH useradmin — a dedicated user the module’s post-install script creates. Root login is disabled; the provider dials admin and wraps every remote command in sudo -n.
Node prepA post-install shell script, not cloud-init — the API exposes no user_data at all. Same shape as the GCE startup-script path; readiness is the /var/lib/shc-node-prepared marker.
Plane NICThe single public interface, eth0 by default (UNVERIFIED-LIVE). There is no second NIC because there is no private network.
DNScloudflare-parent, always. Hostinger’s own DNS is never wired — the module creates no DNS resources. A Hetzner-hosted parent zone works too.
Volume providernone. One fixed disk per VPS, not detachable, not independently resizable. Apps ride node-local docker volumes.
Load-balancer providernone. Ingress is per-node; DNS points at node IPs.
Firewallnone in terraform. The REST API has a firewall product; the provider ships no resource for it. Nodes are unfiltered public boxes.
Tags / labelsnone. The hostname is the only writable identity, so it is the only cloud-side attribution channel.
Delete protectionnone.

Exactly two, and the second one is the overlay.

One VPS, one public IP, ingress and the daemon on the same box. Nothing to mesh. This is the shape the substrate is actually good at.

2. Multi node — topology.kind = "wan" (nebula overlay)

Section titled “2. Multi node — topology.kind = "wan" (nebula overlay)”

There is no private network to put a cluster plane on, so a second node can only reach the first over the public internet. The module therefore stages the nebula binaries automatically as soon as more than one node is declared, and the cluster plane rides the nebula WAN overlay — the same shape the cross-cloud mesh lane and the BYO / home-lab lane use.

node_groups = {
nodes = { count = 2, plan = "hostingercom-vps-kvm1-usd-1m", runtime = "compose" }
}
bootstrap = {
# ...
nebula = true
}
topology = { kind = "wan" }

topology.kind = "lb" is a hard validation error — there is no managed load balancer to point it at.


Be explicit about this before choosing Hostinger for anything real.

No load balancer — cross-node L7 ingress is a known gap

Section titled “No load balancer — cross-node L7 ingress is a known gap”

A multi-node Hostinger cluster has no single ingress address. System Traefik runs where the platform places it, DNS points at node IPs, and there is nothing in front to fail over:

  • a DNS record naming one node is a single point of failure for that host;
  • round-robin A records spread load but do not health-check, so a dead node keeps taking a share of traffic until DNS changes;
  • a request landing on node B for a service running on node A is not transparently proxied. Keep publicly-fronted services on the node DNS points at, or accept the gap.

If you need ingress HA, use an LB-bearing cloud (hetzner, aws, azure, digitalocean).

Each VPS has one fixed disk. It cannot be detached, moved to another node, or grown independently of the plan. There is no SHC volume provider for Hostinger, so apps ride node-local docker volumes and app data lives and dies with its node — the same posture as exoscale and openstack today. Back up accordingly; a node recreate on this substrate is also a fresh purchase.

No firewall in terraform — nodes are unfiltered public boxes

Section titled “No firewall in terraform — nodes are unfiltered public boxes”

Hostinger has a VPS firewall product (/api/vps/v1/firewall), but the terraform provider exposes no resource for it. Every port a container publishes is reachable from the internet. The module refuses firewall.inter_node = "plane" / "none" rather than pretending to honor them. Compensating controls, in order:

  1. run the plane over the nebula overlay (topology.kind = "wan") so cross-node traffic is encrypted end to end;
  2. bind app ports to the overlay or loopback and front them with system Traefik;
  3. configure a host firewall deliberately — noting the post-install script disables the image’s ufw, because a half-configured host firewall silently eats the cluster plane (ICMP passes, so ping lies, while the plane’s TCP/UDP ports are dropped and joins time out X500023JNFAIL).

Reopen this the day the provider grows a firewall resource.

The VPS API carries no metadata field whatsoever. A node cannot be attributed to a cluster from the cloud side except through its hostname, which is why the module writes the sanitized SHC node name there and why the lane’s teardown check and reaper both match on a hostname prefix.


Mint one in the Hostinger panel (VPS → API tokens) and export it:

Terminal window
export HOSTINGER_API_TOKEN=...

It backs the hostinger terraform provider only — there is no SHC hostinger connection type, because there is no proxy or volume backend to connect.

Two things to check before your first apply, both of which will otherwise fail at purchase time:

Terminal window
# 1. A payment method must exist. An empty list means nothing can be bought.
curl -s -H "Authorization: Bearer $HOSTINGER_API_TOKEN" \
https://developers.hostinger.com/api/billing/v1/payment-methods
# 2. The account must not be VIRGIN. On an account owning zero VPSes the
# post-install-script endpoints answer 403 "[VPS:2000] Unauthorized"
# (an ACCOUNT-STATE gate, not a token scope — hPanel tokens have no
# scopes; live-verified 2026-07-31: the 403 flips to 200 the moment
# the first VPS purchase completes). Terraform creates the script
# BEFORE the first VPS, so a from-empty apply on a virgin account
# fails (safely, before money moves). Buy the account's first VPS
# once — panel or API — and every later from-empty apply works.
curl -s -o /dev/null -w '%{http_code}\n' \
-H "Authorization: Bearer $HOSTINGER_API_TOKEN" \
https://developers.hostinger.com/api/vps/v1/post-install-scripts

Hostinger DNS is never wired. Point SHC at a zone you already own — a Cloudflare Zone:DNS:Edit token on the parent zone is the default and the recommended path:

Terminal window
export SHC_TEST_DOMAIN=example.com
export SHC_TEST_CLOUDFLARE_TOKEN=cf-xxxxxxxx

A Hetzner-hosted parent zone works too (SHC_TEST_DNS_PROVIDER=hetzner).


provider "hostinger" {} # token from HOSTINGER_API_TOKEN
module "shc" {
source = "../../modules/shc/hostinger"
# Every node is a >= 1-month, non-refundable subscription.
accept_monthly_billing = true
cluster_name = "demo"
data_center = "fra"
ssh_public_key = file("~/.ssh/id_ed25519.pub")
ssh = { user = "admin", private_key_path = "~/.ssh/id_ed25519" }
node_groups = {
nodes = { count = 1, plan = "hostingercom-vps-kvm1-usd-1m", runtime = "compose" }
}
bootstrap = {
package = { deb_url = "https://.../shc_..._linux_amd64.deb" }
vault_password = var.vault_pw
host = "cloud.example.com"
}
topology = { kind = "lan" }
}

Then wire DNS + ACME as root shc_* resources (a cloudflare shc_connection plus an shc_config), exactly as tests/clouds/hostinger/terraform/main.tf does. There is no cloud connection to add alongside it.


Terminal window
HOSTINGER_API_TOKEN=... CLOUDFLARE_API_TOKEN=... SHC_TEST_DOMAIN=example.com \
SHC_TEST_HOSTINGER_ACCEPT_BILLING=1 \
make test/hostinger # buys one month; provisions, verifies, tears down
make test/hostinger/clean # teardown + an API check that no VPS survives

SHC_TEST_NODE_COUNT=2 runs the overlay cell and buys a second month. Full env contract in tests/clouds/hostinger/README.md.

If a state file is ever lost, the stateless path still lists strays from the prefix alone:

Terminal window
tests/clouds/lib/reap-run.sh --cloud hostinger --prefix shc-hostinger

Verified against hostinger/hostinger 0.1.22 on 2026-07-31.

ResourceNotes
hostinger_vpsplan + data_center_id + template_id required; hostname / password / payment_method_id / post_install_script_id / ssh_key_ids optional. Exports ipv4_address, ipv6_address, status, vps_id. Create is a purchase; destroy is a subscription cancellation.
hostinger_vps_ssh_keyname + key. Account-level. Its material lands on root, which the post-install script then locks out of sshd.
hostinger_vps_post_install_scriptname + content. Account-level, referenced by id — one script serves every node.
hostinger_dns_recordExists, unused by SHC.

Data sources: hostinger_vps_data_centers, hostinger_vps_plans, hostinger_vps_templates. There is no volume, network, firewall or load balancer resource anywhere in the provider.