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.
Cost: there is no hourly billing
Section titled “Cost: there is no hourly billing”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 SKU | vCPU / RAM / disk | first month | renewal |
|---|---|---|---|
hostingercom-vps-kvm1-usd-1m | 1 / 4 GB / 50 GB | $9.99 | $19.49 |
hostingercom-vps-kvm2-usd-1m | 2 / 8 GB / 100 GB | $13.99 | $24.49 |
hostingercom-vps-kvm4-usd-1m | 4 / 16 GB / 200 GB | $25.99 | $42.99 |
hostingercom-vps-kvm8-usd-1m | 8 / 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.)
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 itsSHC_TEST_NODE_COUNTdefaults 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 joinacross 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).
At a glance
Section titled “At a glance”| Concern | Hostinger answer |
|---|---|
| Placement axis | data_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 auth | A single API token (HOSTINGER_API_TOKEN), Bearer-authenticated. |
| Boot image | template_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 user | admin — 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 prep | A 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 NIC | The single public interface, eth0 by default (UNVERIFIED-LIVE). There is no second NIC because there is no private network. |
| DNS | cloudflare-parent, always. Hostinger’s own DNS is never wired — the module creates no DNS resources. A Hetzner-hosted parent zone works too. |
| Volume provider | none. One fixed disk per VPS, not detachable, not independently resizable. Apps ride node-local docker volumes. |
| Load-balancer provider | none. Ingress is per-node; DNS points at node IPs. |
| Firewall | none in terraform. The REST API has a firewall product; the provider ships no resource for it. Nodes are unfiltered public boxes. |
| Tags / labels | none. The hostname is the only writable identity, so it is the only cloud-side attribution channel. |
| Delete protection | none. |
Supported topologies
Section titled “Supported topologies”Exactly two, and the second one is the overlay.
1. Single node — topology.kind = "lan"
Section titled “1. Single node — topology.kind = "lan"”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.
What you give up
Section titled “What you give up”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).
No block volumes
Section titled “No block volumes”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:
- run the plane over the nebula overlay (
topology.kind = "wan") so cross-node traffic is encrypted end to end; - bind app ports to the overlay or loopback and front them with system Traefik;
- 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, sopinglies, while the plane’s TCP/UDP ports are dropped and joins time outX500023JNFAIL).
Reopen this the day the provider grows a firewall resource.
No tags, no labels
Section titled “No tags, no labels”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.
Credentials
Section titled “Credentials”Hostinger API token
Section titled “Hostinger API token”Mint one in the Hostinger panel (VPS → API tokens) and export it:
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:
# 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-scriptsCloudflare API token (for DNS)
Section titled “Cloudflare API token (for DNS)”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:
export SHC_TEST_DOMAIN=example.comexport SHC_TEST_CLOUDFLARE_TOKEN=cf-xxxxxxxxA Hetzner-hosted parent zone works too (SHC_TEST_DNS_PROVIDER=hetzner).
Minimal end-to-end deploy
Section titled “Minimal end-to-end deploy”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.
The live smoke lane
Section titled “The live smoke lane”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 survivesSHC_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:
tests/clouds/lib/reap-run.sh --cloud hostinger --prefix shc-hostingerProvider facts worth knowing
Section titled “Provider facts worth knowing”Verified against hostinger/hostinger 0.1.22 on 2026-07-31.
| Resource | Notes |
|---|---|
hostinger_vps | plan + 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_key | name + key. Account-level. Its material lands on root, which the post-install script then locks out of sshd. |
hostinger_vps_post_install_script | name + content. Account-level, referenced by id — one script serves every node. |
hostinger_dns_record | Exists, 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.
Related
Section titled “Related”- Module:
terraform/modules/shc/hostinger— the deltas table, the sanitizer rules, and the first-apply checklist - Lane:
tests/clouds/hostinger - The overlay shape this substrate depends on:
tests/clouds/mesh - Clouds index — the shared env-var contract every lane honors