Deploying a static site to Hostinger shared hosting
Two folders, both marked do-not-upload. Neither the panel nor the docs will tell you which one is live.
Shared hosting is where most side projects actually live, and it is also where the first hour disappears into working out which directory the web server is really reading. On this account the answer was not guessable, so here is the method rather than the answer.
The setup
A fresh domain added to an existing Hostinger account. SSH is enabled on a non-standard port, which is normal for them:
ssh -p 65002 u000000000@203.0.113.10
Key auth worked immediately, no password prompt, because the key had been added to the account previously. If yours prompts, add the key through the panel first rather than fighting ssh-copy-id.
The trap
Adding a domain created two directories, and both of them contained a zero-byte file called DO_NOT_UPLOAD_HERE:
~/domains/example.com/
DO_NOT_UPLOAD_HERE
public_html/
default.php
~/domains/example-com-000000.hostingersite.com/
DO_NOT_UPLOAD_HERE
public_html/
default.php
The obvious reading is that neither folder is correct and the real root is somewhere else. That reading is wrong. The marker is boilerplate that Hostinger drops into the domain folder itself, one level above public_html. It is telling you not to upload into the parent, not that the domain is wrong.
The second directory is the auto-generated preview subdomain. It is not where you deploy.
Why you cannot infer it from DNS
Checking where the domain points does not help:
$ dig +short example.com A
88.222.243.176
147.79.69.252
Neither of those is the SSH host. Hostinger fronts sites with its own CDN, so the address you connect to over SSH and the address the world resolves are different machines. Seeing an unfamiliar IP here is expected and tells you nothing about the document root.
You can confirm the CDN is in front by looking at the response headers, which carry server: hcdn and an x-hcdn-request-id.
The one command that settles it
Stop reasoning and measure. Write a file with a known string, fetch it over the public domain, delete it:
ssh -p 65002 user@host \
'echo probe-8f2a > ~/domains/example.com/public_html/_probe.txt'
curl -s "https://example.com/_probe.txt?cb=1"
# probe-8f2a
ssh -p 65002 user@host \
'rm -f ~/domains/example.com/public_html/_probe.txt'
The cache-buster on the request matters. The CDN will happily serve a 404 it cached a moment earlier and send you back to guessing.
If the string comes back, that folder is live. If it 404s, try the next candidate. Two minutes, no ambiguity, and it works on any host rather than only this one.
Node is installed, just not where you expect
which node returns nothing on this account, which looks like Node is unavailable. It is not. CloudLinux installs runtimes under /opt/alt, and they are simply absent from the default PATH:
$ /opt/alt/alt-nodejs24/root/usr/bin/node -v
v24.6.0
Versions 18, 20, 22, and 24 were all present. The same pattern applies to PHP, where every release from 5.2 through 8.3 sits under /opt/alt/php*/usr/bin/php.
Worth knowing: the CLI PHP and the web PHP are not the same version. Here php -v reported 8.1.34 while the response header on the live site advertised 8.3.31. If you are debugging behaviour that only reproduces in the browser, that gap is often the reason.
What this means for deploys
Because there is no root, no Docker, and no way to keep a process alive, the sane target for shared hosting is a static build pushed over rsync. The server does nothing but serve files, which also means nothing can break at request time. A failed build fails on your machine, where you can see it, instead of in front of a visitor.
That is the whole deployment story for this site: build locally, sync the output directory, done.
The general rule
On any unfamiliar host, do not infer the document root from the control panel, the folder names, or the DNS records. Write a probe file, request it over the real domain with a cache-buster, and delete it. Guessing costs an hour. Measuring costs two minutes.