Deploying Astro SSR on Hostinger: What the Docs Don't Tell You

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/node adapter (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:

  1. hPanel > Advanced > Node.js App Manager
  2. Create app: entry point server/entry.mjs (dist 기준 상대경로; dist/server/entry.mjs로 입력하면 빌드 실패), Node version 22.x
  3. Set the app root to the deployment directory
  4. Assign a startup command: node server/entry.mjs
  5. 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:

  1. SSH into the server post-deploy and run drizzle-kit migrate manually
  2. 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:

  • .env file 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

  1. Push to main
  2. GitHub Actions: install, build, rsync dist/ + package.json to server
  3. GitHub Actions: SSH restart command to hPanel Node app (npm install && restart)
  4. First request triggers runtime DB migration via middleware
  5. Site live

What Hostinger's Docs Skip

  • The SSH environment has no Node unless you source NVM manually
  • localhost resolves to IPv6 on their servers; use 127.0.0.1
  • DB is not reachable from external IPs — no build-time migration
  • The .htaccess reverse 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

Related content coming soon.