Next.js on Hostinger with GitHub, Cloudflare & AI Search
Introduction
I deliberately chose not to use Vercel for this portfolio. Vercel is excellent. However, free tier limits become an issue quickly for personal projects.
Instead, I built a custom deployment pipeline. It combines three tools:
- Hostinger: Node.js runtime and hosting.
- Cloudflare: DNS, CDN, and DDoS protection.
- Cloudflare Workers AI & Vectorize: Semantic AI search.
This post is a hands-on guide. I document exactly how I built this setup. It covers every command, config, and click. Follow these steps to build the same pipeline for your Next.js project.
The stack:
- Runtime: Next.js 15 with full SSR.
- Package manager:
pnpm. - Hosting: Hostinger Web App deployment.
- DNS & CDN: Cloudflare free plan.
- AI search: Cloudflare Workers AI (
@cf/baai/bge-small-en-v1.5) and Vectorize.
Phase 1: Preparing the Repository
Step 1: Push Code to GitHub
Hostinger pulls your code directly from a GitHub repository. Push your Next.js project to GitHub before doing anything else.
git init
git add .
git commit -m "initial commit"
git remote add origin https://github.com/YOUR_USERNAME/YOUR_REPO.git
git push -u origin main
Your repository can be public or private. Hostinger handles both formats smoothly.
Step 2: Set Up pnpm as the Package Manager
I use pnpm instead of npm. This is a hard requirement because of Hostinger's inode limits. I will explain inodes in the next step.
Make sure your project declares pnpm as the package manager in package.json:
{
"packageManager": "pnpm@10.0.0"
}
You must also commit a pnpm-lock.yaml file. Hostinger runs pnpm install --frozen-lockfile during the build. The build fails if the lockfile is missing.
pnpm install
git add pnpm-lock.yaml
git commit -m "add pnpm lockfile"
git push
Step 3: Add a prebuild Script for Inode Control
This is the most important lesson I learned with Hostinger. Hostinger limits the total number of files on your account. These files are called inodes.
Next.js generates thousands of tiny files in .next/cache and node_modules/.cache during every build. You will hit the inode limit after a few deployments. Then, your deployments will fail silently.
The fix is adding a prebuild script to package.json. This script wipes the cache directories before every build:
{
"scripts": {
"prebuild": "rm -rf .next/cache && rm -rf node_modules/.cache",
"build": "next build",
"start": "next start"
}
}
Node package managers automatically run any script named prebuild before running build. Hostinger will clear the old cache before building the new version. This keeps your total file count flat over time.
Commit this change before moving forward:
git add package.json
git commit -m "add prebuild cache cleanup for inode control"
git push
Phase 2: Deploying on Hostinger
Step 4: Create a Web App in hPanel
- Log into your Hostinger account and open hPanel.
- Go to Websites in the sidebar.
- Click Add website or Deploy Your Web App.
- You will see an Import Git repository option. Click Continue with GitHub.
- Authorize Hostinger to access your repositories on the GitHub OAuth page.
- Return to hPanel and select your repository from the dropdown menu.
- Select your main branch. Mine is
main.
Step 5: Configure the Build Settings
Hostinger detects your Next.js application automatically. It pre-fills the build and start commands. Check them carefully:
| Setting | Value |
|---|---|
| Build command | pnpm install && pnpm build |
| Start command | pnpm start |
| Node.js version | 22.x |
| Output directory | .next |
Change the commands manually if Hostinger defaults to npm.
The prebuild script handles the cache cleanup automatically. You do not need to add rm -rf commands here.
Step 6: First Deployment
Click Deploy. Hostinger will execute these steps:
- Clone your repository.
- Run
pnpm install --frozen-lockfile. - Run the
prebuildscript to clear cache. - Run
pnpm build. - Start the server with
pnpm start.
The first build takes a few minutes. You can monitor the live logs in hPanel. Hostinger provides a temporary .hostingersite.com subdomain for testing when the build finishes.
Step 7: Automatic Deployments
Hostinger creates a GitHub webhook automatically. Any push to your selected branch triggers a new deployment.
Verify this is active:
- Go to your GitHub repository.
- Navigate to Settings then Webhooks.
- Look for a Hostinger webhook URL with a green checkmark.
Every time I run git push origin main, Hostinger pulls the code, builds it, and deploys it automatically.
Phase 3: Pointing Your Domain Through Cloudflare
I use Cloudflare for three reasons:
- Global CDN caching
- Free automatic HTTPS
- DDoS protection
The traffic flow is simple: User → Cloudflare edge → Hostinger.
Step 8: Add Your Site to Cloudflare
- Create a free account at dash.cloudflare.com.
- Click Add a Site and enter your domain name.
- Select the Free plan.
- Cloudflare scans your existing DNS records. Ensure your Hostinger A record is listed. Add it manually if it is missing.
Step 9: Update Your Domain's Nameservers
Cloudflare assigns you two custom nameservers. They look like this:
amy.ns.cloudflare.com
bob.ns.cloudflare.com
Go to your domain registrar. Replace the existing nameservers with the Cloudflare nameservers. Save your changes.
DNS propagation usually finishes in under 30 minutes.
Step 10: Verify DNS Records in Cloudflare
Go to the DNS tab in Cloudflare after propagation finishes. Check your records:
| Type | Name | Content | Proxy |
|---|---|---|---|
| A | @ |
Hostinger's server IP | Proxied (orange cloud) |
| CNAME | www |
yourdomain.com |
Proxied (orange cloud) |
The orange cloud icon confirms traffic is routing through Cloudflare.
Step 11: SSL and Performance Settings
Adjust these settings in Cloudflare:
- Go to SSL/TLS. Set encryption mode to Full (strict).
- Go to Speed then Optimization. Enable Auto Minify for JavaScript, CSS, and HTML.
- Go to Caching then Configuration. Set the Browser Cache TTL to 4 hours.
Your site now serves over HTTPS globally.
Phase 4: AI-Powered Blog Search
This portfolio uses semantic vector embeddings. Every blog post is indexed and stored in Cloudflare Vectorize.
I built two AI features using this data:
- AI Search (
/api/ai-search): Readers ask questions and get relevant blog posts based on meaning, not just keywords. - Per-post AI Chat (
/api/blog-chat): An AI assistant on each page answers questions about the specific post.
These features use standard Next.js API routes. They do not require a separate Cloudflare Worker. Hostinger runs the full Node.js runtime, allowing direct REST API calls.
Step 12: The Indexing Script
I pre-process every blog post and convert the text into vectors. I use a Node.js script located at scripts/index-blog-embeddings.js.
The script processes every .mdx file:
1. Parse and clean the text:
const { data, content } = matter(rawFile);
const cleanContent = removeMd(
content
.replace(/```[\s\S]*?```/g, "") // strip code blocks
.replace(/<[\s\S]*?>/g, "") // strip HTML tags
);
I strip out code blocks and HTML. I only want the core prose for semantic search.
2. Split into chunks:
function chunkText(text, minLength = 300, maxLength = 1000) {
const paragraphs = text.split(/
+/);
// ... builds chunks that respect paragraph boundaries
}
I split each post into paragraph-aware chunks. Each chunk is between 300 and 1000 characters.
3. Generate embeddings with Cloudflare Workers AI:
const url = `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/ai/run/@cf/baai/bge-small-en-v1.5`;
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${API_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ text: [chunk] }),
});
const embedding = json.result.data[0]; // 384-dimension float array
I use Cloudflare's hosted bge-small-en-v1.5 model. It is fast and requires no local ML infrastructure.
4. Upsert into Vectorize:
const url = `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/vectorize/v2/indexes/blog-search/upsert`;
const vectors = chunks.map((chunk, j) => ({
id: `${slug}#${j}`,
values: embedding,
metadata: { slug, title, text: chunk, chunkIndex: j },
}));
The vector IDs use the format slug#chunkIndex. The script detects if a post gets shorter and deletes orphaned vectors automatically.
5. Content-hash caching:
const contentHash = crypto.createHash("sha256").update(raw, "utf8").digest("hex");
const cached = indexCache[slug];
if (cached && cached.hash === contentHash) {
skippedCount++;
continue; // nothing changed, skip this post entirely
}
I use a SHA-256 hash instead of file modification times. Git clone resets file times on every deploy. A hash ensures we only re-index actual content changes.
Step 13: Why Use GitHub Actions?
Many people ask why I run embeddings in GitHub Actions instead of Hostinger. There are three reasons:
1. Build Time Limits. Generating embeddings requires remote API calls. Doing this during the Hostinger build would add minutes to the process and cause timeouts.
2. Avoiding Redundant Indexing. Hostinger runs a fresh git clone on every deploy. Running the script there would trigger unnecessary API calls. GitHub Actions only triggers when an .mdx file changes.
3. Separation of Concerns. Hostinger serves the application. Cloudflare stores the index. The indexing script is a background data migration job. It belongs in CI.
Step 14: The GitHub Actions Workflow
The workflow file is located at .github/workflows/index-blog-embeddings.yml:
name: Index Blog Embeddings
on:
push:
branches: ["main"]
paths:
- "content/**/*.mdx"
concurrency:
group: index-blog-embeddings-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
jobs:
index:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- name: Index new/changed blog posts
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: pnpm index:embeddings
- name: Commit updated cache
run: |
if ! git diff --quiet -- scripts/.indexed-posts-cache.json; then
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add scripts/.indexed-posts-cache.json
git commit -m "chore: update blog embeddings cache [skip ci]"
git push
else
echo "No cache changes to commit."
fi
The [skip ci] tag is essential. It prevents the bot commit from triggering an infinite loop of GitHub Actions.
Step 15: Setting Up Cloudflare Secrets
Add these secrets to GitHub under Settings > Secrets and variables > Actions:
| Secret name | How to get it |
|---|---|
CLOUDFLARE_API_TOKEN |
Cloudflare Dashboard → My Profile → API Tokens → Create Token → select Cloudflare AI, Vectorize (Edit permission) |
CLOUDFLARE_ACCOUNT_ID |
Cloudflare Dashboard → home page → right sidebar, listed under "Account ID" |
Step 16: First Run
Run the indexer locally the first time to bootstrap the database:
CLOUDFLARE_API_TOKEN=your_token \
CLOUDFLARE_ACCOUNT_ID=your_account_id \
pnpm index:embeddings
The script will create the blog-search index and upload all vectors. Commit the cache file when it finishes:
git add scripts/.indexed-posts-cache.json
git commit -m "chore: initial blog embeddings cache"
git push
Step 17: The AI Search Route
The /api/ai-search route handles user queries. It performs three steps:
Step A: Embed the query
const embeddingUrl = `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/ai/run/@cf/baai/bge-small-en-v1.5`;
const embeddingRes = await fetch(embeddingUrl, {
method: "POST",
headers: { Authorization: `Bearer ${API_TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify({ text: [query] }),
});
const queryVector = embeddingJson.result.data[0];
Step B: Query Vectorize
const vectorizeUrl = `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/vectorize/v2/indexes/blog-search/query`;
const vectorizeRes = await fetch(vectorizeUrl, {
method: "POST",
headers: { Authorization: `Bearer ${API_TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify({
vector: queryVector,
topK: 8,
returnValues: false,
returnMetadata: "all",
}),
});
Step C: Group and return
The route groups matching chunks by post slug. It ranks them by similarity score and returns the results to the frontend.
Step 18: The Per-Post AI Chat Route
The /api/blog-chat route powers the chat widget on each post. It reads the MDX file from disk instead of querying Vectorize.
Read and clean the post:
const rawFile = fs.readFileSync(filePath, "utf8");
const { data, content } = matter(rawFile);
const cleanContent = removeMd(content.replace(/```[\s\S]*?```/g, "").replace(/<[\s\S]*?>/g, ""));
Build the system prompt:
const systemPrompt = `
You are Ranti's Blog AI assistant. Your goal is to help the reader understand the blog post titled "${title}".
Speak in the first person (as Ranti, the author of this post). Warm, friendly, professional.
Here is the complete text of the blog post:
=========================================
Title: ${title}
Date: ${data.publishedAt}
Content:
${cleanContent}
=========================================
Instructions:
- Answer questions directly related to this blog post.
- Always speak in the first person (e.g. "In my article...", "I wrote this because...").
- Keep answers helpful, concise, and focused on the article.
`;
Send to Cloudflare Workers AI:
const model = "@cf/meta/llama-3.1-8b-instruct";
const url = `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/ai/run/${model}`;
const response = await fetch(url, {
method: "POST",
headers: { Authorization: `Bearer ${API_TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify({
messages: [{ role: "system", content: systemPrompt }, ...messages],
max_tokens: maxTokens,
}),
});
I adjust max_tokens dynamically based on the query complexity. This saves Cloudflare AI quota on simple questions.
How It All Fits Together
Here is the data flow for the AI search:
flowchart TD
A[Reader types a search query] --> B[Next.js /api/ai-search route on Hostinger]
B --> C[Cloudflare Workers AI\nbge-small-en-v1.5\nEmbed the query into a 384-dim vector]
C --> D[Cloudflare Vectorize\nFind top-8 matching post chunks\nby cosine similarity]
D --> E[Group chunks by post slug\nRank by highest similarity score]
E --> F[Return ranked post list with snippets\nto the frontend]Here is the data flow for the per-post AI chat:
flowchart TD
A[Reader asks a question on a blog post page] --> B[Next.js /api/blog-chat route on Hostinger]
B --> C[Read the full MDX post from disk\nStrip markdown and code blocks]
C --> D[Build system prompt\nFull post text injected as context]
D --> E[Cloudflare Workers AI\nLlama 3.1 8B Instruct\nGenerate a grounded answer]
E --> F[Return AI response to the reader]Here is the overall deployment and indexing architecture:
flowchart LR
subgraph GitHub
A[Push to main] --> B{Which files changed?}
B -- MDX file changed --> C[GitHub Actions\nIndex Blog Embeddings workflow]
B -- Code changed --> D[Hostinger webhook\ntriggers redeploy]
end
subgraph Cloudflare
C --> E[Workers AI\nbge-small-en-v1.5\nGenerate embeddings]
E --> F[Vectorize\nblog-search index\nStore vectors]
C --> G[Commit updated cache\nwith skip ci]
end
subgraph Hostinger
D --> H[pnpm install\nprebuild clears cache\npnpm build\npnpm start]
H --> I[Next.js running\nSSR and API routes live]
end
I -- Search query --> E
F -- Top-k matches --> IConclusion
This complete stack provides a production-grade, globally distributed, AI-powered portfolio.
- Hostinger: Node.js runtime.
- Cloudflare: DNS, CDN, AI, and Vectorize.
- GitHub Actions: Automated indexing.
The core insight is that Cloudflare's AI tools are REST APIs. You can call them from any server, including a Next.js API route on Hostinger.
- Deployments: Fully automated on every push.
- Inode safety: Handled by the
prebuildscript andpnpm. - AI indexing: Runs purely in GitHub Actions.
- Search and Chat: Powered by Next.js API routes hitting Cloudflare.
I highly recommend this setup if you want full control and AI capabilities without Vercel lock-in.
FAQ
Can you host Next.js on Hostinger?
Yes. Hostinger's Deploy Your Web App feature supports full Node.js runtimes. You can deploy Next.js with SSR, API routes, and server components directly. Connect your GitHub repository, and Hostinger handles the build.
Does Hostinger support Node.js and Next.js natively?
Yes. Hostinger runs Node.js natively on its Web Hosting plans. It supports Next.js, Nuxt, and Express. You do not need a VPS.
Why should I use pnpm instead of npm on Hostinger?
Use pnpm on Hostinger to manage strict inode limits. Standard npm creates tens of thousands of individual files. pnpm uses a global content store with symlinks. This drastically cuts the real file count and prevents inode ceiling errors.
How do I fix Hostinger inode limit issues with Next.js?
Add a prebuild script to your package.json: "prebuild": "rm -rf .next/cache && rm -rf node_modules/.cache". Switch from npm to pnpm. These two steps keep your inode usage flat across multiple deployments.
Is Cloudflare free to use with Hostinger?
Yes. Cloudflare's free plan works seamlessly with Hostinger. Point your domain's nameservers at Cloudflare. It proxies traffic to your server, providing free SSL, global CDN caching, and DDoS protection.
How do I add AI search to a Next.js blog?
Use Cloudflare Workers AI to generate text embeddings for each post. Store them in Cloudflare Vectorize. When a user searches, embed the query and query Vectorize for similar chunks. Return the ranked results. No GPU server is required.
Does Cloudflare Workers AI work without a Cloudflare Worker?
Yes. Cloudflare Workers AI offers a standard REST API. Any server that makes HTTPS requests can call it directly. A Next.js API route on Hostinger works perfectly.
How do I auto-index blog posts to Cloudflare Vectorize on every publish?
Create a GitHub Actions workflow. Trigger it on push to main with paths restricted to content/**/*.mdx. Run your embedding script to generate vectors and upsert them to Vectorize. Use a content-hash cache file to skip unchanged posts. Commit the cache back with a [skip ci] tag.
Was this post helpful?
Your feedback helps me improve future content
Keep Reading

Goodbye reCAPTCHA, Hello Turnstile: Why I Switched (Next.js Guide)
Running Local LLM Agents in Kubernetes: A Practitioner's Guide to vLLM on EKS
Kiro vs Cursor vs Windsurf vs Claude Code vs Codex vs Antigravity: What I Actually Use as an SRE
Subscribe to Newsletter
Get the latest posts delivered right to your inbox
Join 1,000+ readers. No spam, unsubscribe anytime.