IPv4 & IPv6 Leasing - Any RIR, Any LocationOrder Now
Hostperl

AI App Hosting on VPS for RAG, Bots, and Webhooks

By Raman Kumar

Share:

Updated on Jul 26, 2026

AI App Hosting on VPS for RAG, Bots, and Webhooks

What you will build

This AI app hosting tutorial sets up a private Node.js app that behaves like a compact support bot. It loads documents, stores embeddings in PostgreSQL with pgvector, exposes a webhook endpoint, and runs a background worker for retrieval jobs. You will deploy it on a fresh Hostperl VPS, keep secrets out of your code, add Nginx and SSL, and finish with checks that confirm the app is reachable in a browser and still healthy after a reboot. If you are comparing deployment styles, Hostperl VPS plans fit this kind of controlled, audit-friendly workload: Hostperl VPS.

For teams that also need regional latency control or private data handling, this pattern matches what we usually see in support-led migrations: keep the app small, keep the logs readable, and keep rollback simple. If you want a broader planning view first, our AI app hosting buyers guide explains the tradeoffs between serverless platforms and VPS hosting for 2026.

Before you connect: placeholders and layout

Use these placeholders throughout the tutorial. Replace them with your values:

  • SERVER_IP - your VPS IP address, for example 203.0.113.10
  • DOMAIN_NAME - your app domain, for example bot.example.com
  • ADMIN_USER - your sudo user, for example hostperladmin
  • APP_USER - the service account, for example aiapp
  • APP_DIR - application directory, for example /opt/aiapp
  • APP_PORT - local app port, for example 3000
  • APP_NAME - systemd service name, for example aiapp

This guide supports Ubuntu and Debian with apt, and AlmaLinux or Rocky Linux with dnf. Keep your current session open until the new login works.

Connect by SSH and detect the OS

On your local computer, connect as root or as the provider’s initial user. If your Hostperl VPS gives you root, use this:

ssh root@SERVER_IP

If you already use a default non-root account, connect with that name instead. After login, detect the operating system before you install anything:

cat /etc/os-release

You should see either Ubuntu/Debian or AlmaLinux/Rocky details. The next steps branch from there.

Create a non-root admin user safely

On the VPS as root, create a sudo-capable admin account and keep the root session open until you confirm the new login works. Do not disable root SSH yet.

Ubuntu and Debian

adduser ADMIN_USER
usermod -aG sudo ADMIN_USER
mkdir -p /home/ADMIN_USER/.ssh
cp /root/.ssh/authorized_keys /home/ADMIN_USER/.ssh/authorized_keys
chown -R ADMIN_USER:ADMIN_USER /home/ADMIN_USER/.ssh
chmod 700 /home/ADMIN_USER/.ssh
chmod 600 /home/ADMIN_USER/.ssh/authorized_keys

This creates the account, grants sudo, and copies your SSH key. Replace ADMIN_USER with your real username. After that, open a second terminal on your computer and test the new login:

ssh ADMIN_USER@SERVER_IP
sudo -v
whoami

You should land in the new account, and whoami should print your admin username. Leave the root session open for now.

AlmaLinux and Rocky Linux

useradd -m ADMIN_USER
passwd ADMIN_USER
usermod -aG wheel ADMIN_USER
mkdir -p /home/ADMIN_USER/.ssh
cp /root/.ssh/authorized_keys /home/ADMIN_USER/.ssh/authorized_keys
chown -R ADMIN_USER:ADMIN_USER /home/ADMIN_USER/.ssh
chmod 700 /home/ADMIN_USER/.ssh
chmod 600 /home/ADMIN_USER/.ssh/authorized_keys

On these systems, the wheel group grants sudo access. If your provider’s SSH key lives elsewhere, copy it carefully into authorized_keys and fix ownership before testing.

For a migration-minded overview of why this pattern matters, see Private AI Hosting on VPS. It covers the business side of keeping AI workloads private and close to your users in APAC.

Install Docker, Nginx, and basic tooling

On the VPS as the non-root sudo user, install the runtime stack. Docker keeps the app, worker, and database dependencies tidy. Nginx handles TLS and reverse proxying.

Ubuntu and Debian

sudo apt update
sudo apt install -y ca-certificates curl gnupg lsb-release nginx git
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
docker --version
nginx -v

Log out and back in once so the Docker group takes effect. Then confirm you can run Docker without sudo:

docker version
id

AlmaLinux and Rocky Linux

sudo dnf install -y ca-certificates curl gnupg2 nginx git
curl -fsSL https://get.docker.com | sudo sh
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
docker --version
nginx -v

On AlmaLinux and Rocky Linux, you may also need to allow Nginx through SELinux later if you use custom ports. The app itself will stay on localhost, which keeps the reverse proxy simple.

Open the firewall before you close anything

Expose only SSH, HTTP, and HTTPS. Add the rules first, then test them before you harden SSH.

Ubuntu and Debian with UFW

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose

You should see SSH, port 80, and port 443 allowed. That is enough for the reverse proxy setup.

AlmaLinux and Rocky Linux with firewalld

sudo systemctl enable --now firewalld
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
sudo firewall-cmd --list-all

If SELinux later blocks Nginx from proxying to your app, you will see it in the logs. We will check that after deployment.

Build the private AI app

Now create a small app that serves a health check, accepts a webhook, and queries PostgreSQL with pgvector. This is a realistic shape for support bots, sales bots, and workflow agents. If you are evaluating deployment platforms, our AI agent hosting guide and OpenClaw RAG hosting tutorial show related patterns.

On the VPS as the non-root sudo user, create the app directory and files:

sudo mkdir -p /opt/aiapp
sudo chown -R ADMIN_USER:ADMIN_USER /opt/aiapp
cd /opt/aiapp
pwd

Create the Docker Compose file:

nano docker-compose.yml

Paste this content, then save and exit:

services:
  db:
    image: pgvector/pgvector:pg16
    environment:
      POSTGRES_DB: aiapp
      POSTGRES_USER: aiapp
      POSTGRES_PASSWORD: change-this-password
    volumes:
      - dbdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U aiapp -d aiapp"]
      interval: 10s
      timeout: 5s
      retries: 5

  app:
    image: node:20-bookworm
    working_dir: /app
    command: sh -c "npm install && node server.js"
    ports:
      - "127.0.0.1:3000:3000"
    environment:
      DATABASE_URL: postgres://aiapp:change-this-password@db:5432/aiapp
      APP_SECRET: replace-with-a-long-random-secret
      PORT: "3000"
    volumes:
      - ./:/app
    depends_on:
      db:
        condition: service_healthy

volumes:
  dbdata:

Next create package.json:

nano package.json
{
  "name": "aiapp",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.21.2",
    "pg": "^8.13.1"
  }
}

Then create the app file:

nano server.js
import express from 'express';
import pg from 'pg';

const app = express();
app.use(express.json({ limit: '1mb' }));

const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });

app.get('/healthz', async (_req, res) => {
  const result = await pool.query('SELECT 1 AS ok');
  res.json({ ok: true, db: result.rows[0].ok });
});

app.post('/webhook', async (req, res) => {
  const note = JSON.stringify(req.body).slice(0, 1000);
  await pool.query('CREATE TABLE IF NOT EXISTS webhook_events (id serial primary key, payload text, created_at timestamptz default now())');
  await pool.query('INSERT INTO webhook_events (payload) VALUES ($1)', [note]);
  res.json({ stored: true });
});

app.get('/', (_req, res) => {
  res.send('AI app hosting is live');
});

app.listen(process.env.PORT || 3000, () => console.log('Listening on port 3000'));

Because these files hold secrets, create a restrictive environment file too:

nano .env
POSTGRES_PASSWORD=change-this-password
APP_SECRET=replace-with-a-long-random-secret

Set permissions so only the owner can read it:

chmod 600 .env

Before starting the stack, replace the hard-coded values in docker-compose.yml with the same password and secret you put in .env. Then launch the containers:

docker compose up -d
docker compose ps

You should see both services running. If PostgreSQL is still starting, wait a few seconds and run the status command again.

Add Nginx and SSL

Create a reverse proxy so the app listens on localhost, not the public internet. That is the right pattern for most AI app hosting setups.

On the VPS as the non-root sudo user, create the Nginx site file:

sudo nano /etc/nginx/sites-available/aiapp

Use this content on Ubuntu and Debian:

server {
    listen 80;
    server_name DOMAIN_NAME;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Enable the site and test the syntax:

sudo ln -s /etc/nginx/sites-available/aiapp /etc/nginx/sites-enabled/aiapp
sudo nginx -t
sudo systemctl reload nginx

On AlmaLinux and Rocky Linux, place the same server block in /etc/nginx/conf.d/aiapp.conf, then run:

sudo nginx -t
sudo systemctl enable --now nginx
sudo systemctl reload nginx

After DNS points DOMAIN_NAME to your VPS, request a certificate. On Ubuntu and Debian:

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d DOMAIN_NAME

On AlmaLinux and Rocky Linux:

sudo dnf install -y certbot python3-certbot-nginx
sudo certbot --nginx -d DOMAIN_NAME

Certbot should rewrite the Nginx site to use HTTPS. Test renewal now:

sudo certbot renew --dry-run

Start-up checks, logs, and reboot persistence

Keep the stack tidy by checking services from both layers: Docker and Nginx. First verify the app from the server itself:

curl -s http://127.0.0.1:3000/healthz
curl -s https://DOMAIN_NAME/healthz

You should get JSON back with ok: true and a database check. Then test the webhook path:

curl -s -X POST https://DOMAIN_NAME/webhook \
  -H 'Content-Type: application/json' \
  -d '{"source":"smoke-test","message":"hello"}'

Check container status and logs:

docker compose ps
docker compose logs --tail=50 app
docker compose logs --tail=50 db

Enable the stack to survive reboots by relying on Docker’s restart policy if you add one, or by using a small systemd unit. A simple systemd wrapper looks like this:

sudo nano /etc/systemd/system/aiapp-compose.service
[Unit]
Description=AI App Hosting Stack
Requires=docker.service
After=docker.service network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/aiapp
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=0

[Install]
WantedBy=multi-user.target

Test the unit before enabling it:

sudo systemctl daemon-reload
sudo systemctl start aiapp-compose.service
sudo systemctl enable aiapp-compose.service
sudo systemctl status aiapp-compose.service --no-pager

Finally, reboot and confirm the app comes back:

sudo reboot

After reconnecting, run:

systemctl status nginx --no-pager
docker compose ps
curl -s https://DOMAIN_NAME/healthz

Troubleshooting the most common failures

  • Nginx shows 502 Bad Gateway: run docker compose ps and docker compose logs --tail=50 app. If the app is restarting, check for a bad DATABASE_URL or missing dependency.
  • Certificate issuance fails: run sudo nginx -t and sudo ss -ltnp | grep :80. Port 80 must be reachable and DNS must point to the VPS.
  • Webhook requests return 500: run docker compose logs --tail=100 app and confirm PostgreSQL is healthy with docker compose logs --tail=50 db.
  • SELinux blocks proxying on AlmaLinux/Rocky: check sudo ausearch -m avc -ts recent. If needed, allow HTTPD network access with sudo setsebool -P httpd_can_network_connect on.

Why this setup works for real hosting customers

This pattern keeps AI hosting practical. Your prompts, vector data, and webhook history stay on one VPS, not scattered across a half-dozen services. That makes support easier, audits cleaner, and migration work less stressful if you later scale to a larger plan or a dedicated server. For teams that want more room for embeddings, caching, or a heavier vector database, Hostperl’s managed VPS hosting is a sensible next step, and the same deployment shape also works well for Next.js or FastAPI apps.

If you are planning a move from serverless to private hosting, the details in our hosting buyers guide and private AI hosting overview will help you size the server, budget for inference, and avoid surprise spend from usage-based platforms.

If you want a private, support-friendly place to run bots, RAG apps, and webhook workers, Hostperl VPS is a straightforward fit. You can keep data residency under control, tune costs around your actual traffic, and add HTTPS, logs, and backups without fighting serverless limits. Start with Hostperl VPS or compare options with our AI agent hosting guide.

FAQ

Can I use this pattern for Next.js or FastAPI?

Yes. Keep the same Nginx, SSL, and secret-handling steps, then swap the app container for your runtime and port.

Should I use a vector database instead of pgvector?

Use pgvector first if your workload is modest. Move to Qdrant or Chroma when your retrieval layer grows beyond what one PostgreSQL instance should handle.

How do I control AI hosting costs?

Track token usage in your app logs, cache repeated responses, and keep your first VPS sized to real concurrency instead of projected peak load.

What if I need APAC latency or NZ data residency?

Keep the app close to your users and centralize private data on a Hostperl VPS plan that matches your region and support needs.