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_IPIf 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-releaseYou 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_keysThis 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
whoamiYou 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_keysOn 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 -vLog out and back in once so the Docker group takes effect. Then confirm you can run Docker without sudo:
docker version
idAlmaLinux 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 -vOn 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 verboseYou 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-allIf 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
pwdCreate the Docker Compose file:
nano docker-compose.ymlPaste 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.jsimport 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 .envPOSTGRES_PASSWORD=change-this-password
APP_SECRET=replace-with-a-long-random-secretSet permissions so only the owner can read it:
chmod 600 .envBefore 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 psYou 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/aiappUse 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 nginxOn 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 nginxAfter 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_NAMEOn AlmaLinux and Rocky Linux:
sudo dnf install -y certbot python3-certbot-nginx
sudo certbot --nginx -d DOMAIN_NAMECertbot should rewrite the Nginx site to use HTTPS. Test renewal now:
sudo certbot renew --dry-runStart-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/healthzYou 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 dbEnable 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-pagerFinally, reboot and confirm the app comes back:
sudo rebootAfter reconnecting, run:
systemctl status nginx --no-pager
docker compose ps
curl -s https://DOMAIN_NAME/healthzTroubleshooting the most common failures
- Nginx shows 502 Bad Gateway: run
docker compose psanddocker compose logs --tail=50 app. If the app is restarting, check for a badDATABASE_URLor missing dependency. - Certificate issuance fails: run
sudo nginx -tandsudo 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 appand confirm PostgreSQL is healthy withdocker compose logs --tail=50 db. - SELinux blocks proxying on AlmaLinux/Rocky: check
sudo ausearch -m avc -ts recent. If needed, allow HTTPD network access withsudo 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.
