Hostinger Business hosting. Astro v6 with SSR. Node adapter. The setup looked straightforward. It wasn't.
This is the deployment log — errors, fixes, and commands in the order they happened.
The Stack
- Astro v6 with
@astrojs/nodeadapter (standalone mode) - Drizzle ORM + MySQL (Hostinger's managed DB)
- GitHub Actions for CI/CD
- Hostinger Business shared hosting (hPanel)
Step 1: The Build Passed. The Deploy Did Nothing.
First push to the Actions workflow. Build succeeded. Deployment via SSH rsync. Files landed on the server. Site didn't load.
The issue: Hostinger Business runs Apache with LiteSpeed, not a raw Node server. There's no process manager pre-configured. You need to set up the Node.js app manually in hPanel under Node.js App Manager.
Steps that worked:
- hPanel > Advanced > Node.js App Manager
- Create app: entry point
server/entry.mjs(dist 기준 상대경로;dist/server/entry.mjs로 입력하면 빌드 실패), Node version 22.x - Set the app root to the deployment directory
- Assign a startup command:
node server/entry.mjs - Restart app after each deploy
The .htaccess reverse proxy was also required to route traffic from the domain to the Node process port. Hostinger generates this automatically from the Node.js App Manager — do not overwrite it.
Step 2: SSH Had No Node
GitHub Actions workflow ran npm ci over SSH for the dependency install step. Error:
bash: node: command not found
Hostinger's SSH environment does not inherit the Node version set in hPanel. It runs a minimal shell without the app-scoped Node on the PATH.
Fix: source the NVM path manually in the SSH command, or skip the SSH npm ci entirely and rsync the pre-built dist/ from the CI runner instead.
The approach that worked: build fully in GitHub Actions, then rsync dist/, package.json, and package-lock.json. Install dependencies on the server via the Node.js App Manager restart (which runs npm install against the deployed package.json).
Revised workflow step:
- name: Sync build to server
run: |
rsync -avz --delete \
dist/ package.json package-lock.json \
${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:${{ secrets.DEPLOY_PATH }}/
Step 3: DB Connection Refused at Build Time
Early pipeline version attempted a Drizzle migration during the build step. The build runner cannot reach the Hostinger MySQL instance.
Error:
Error: connect ECONNREFUSED 127.0.0.1:3306
Hostinger's DB is only accessible from the hosting server itself, not from an external IP or GitHub's runners. The build environment is not the runtime environment.
Two approaches considered:
- SSH into the server post-deploy and run
drizzle-kit migratemanually - Auto-migrate at runtime on first request
Manual SSH migration also failed — the SSH environment had no npx on PATH and the DB credentials weren't available in the shell environment without explicit export.
The working solution: runtime migration via middleware. Covered in the companion post Runtime Migration: When Your Build Can't Touch the Database.
Step 4: IPv6 Rejection
After getting the runtime migration approach in place, the first live request hit a new error:
Error: connect ECONNREFUSED ::1:3306
Node was resolving localhost to ::1 (IPv6 loopback). Hostinger's MySQL does not listen on IPv6.
Fix: force the DB connection string to 127.0.0.1 instead of localhost.
// db/index.ts
const connection = mysql.createConnection({
host: '127.0.0.1', // not 'localhost'
port: 3306,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
});
One character difference. Two hours of elimination to find it.
Step 5: Environment Variables
Hostinger's Node.js App Manager has an environment variable section in hPanel. Variables set there are injected at runtime. They are not available during SSH sessions.
The pattern that worked:
.envfile not used on the server (security)- Variables set in hPanel Node.js App Manager UI
- GitHub Actions secrets used for the CI/CD credentials (SSH key, deploy path)
- Runtime code reads
process.env.*— no dotenv needed in production
Final Deployment Flow
- Push to
main - GitHub Actions: install, build, rsync
dist/+package.jsonto server - GitHub Actions: SSH restart command to hPanel Node app (
npm install && restart) - First request triggers runtime DB migration via middleware
- Site live
What Hostinger's Docs Skip
- The SSH environment has no Node unless you source NVM manually
localhostresolves to IPv6 on their servers; use127.0.0.1- DB is not reachable from external IPs — no build-time migration
- The
.htaccessreverse proxy must not be overwritten; it's generated by hPanel - Node.js App Manager restart is required after every deploy for dependency changes
The Astro Node adapter works fine on Hostinger Business once these constraints are understood. The docs assume a VPS with full environment control. Shared hosting is different.
Comments 0