Private AI App Hosting on VPS with RAG and Security

What you'll build
Private AI app hosting works better than serverless for many support bots, sales assistants, and RAG apps. You keep traffic close to users, store data in a known region, and avoid surprise bills when token usage climbs.
In this tutorial, you'll deploy a compact private AI app stack on a fresh Hostperl VPS with Docker Compose, Nginx, and PostgreSQL with pgvector. The setup fits internal assistants, customer-service automations, webhook handlers, and lightweight retrieval pipelines. If you want to compare approaches first, see AI bot hosting decisions for VPS, RAG, and private deployments and private AI hosting cost, security, and latency.
For teams that need predictable performance, a Hostperl VPS sits in the middle ground between managed convenience and full control. It also makes sense if you're moving away from Vercel, Render, or Railway and want fewer surprises around background jobs, data locality, and API key handling.
Prerequisites and placeholders
Use these placeholders throughout the guide:
SERVER_IP= your VPS public IP, for example203.0.113.10DOMAIN_NAME= the hostname for the app, for exampleai.example.comADMIN_USER= the non-root sudo user you create, for examplealexAPP_USER= the service account for the app, for exampleaiappAPP_DIR= the deployment directory, for example/opt/ai-appAPP_PORT= the local app port, for example3000
This tutorial covers Ubuntu/Debian and AlmaLinux/Rocky Linux separately where commands differ. Keep your first root session open until the new sudo user works from a second terminal.
Connect to the VPS and check the OS
On your local computer, SSH into the server. If your provider gave you a default non-root user, use that instead of root.
ssh root@SERVER_IPYou should land in a root shell on the VPS. If your provider uses a default account such as ubuntu, use ssh ubuntu@SERVER_IP.
On the VPS as root, detect the distribution before you install anything.
cat /etc/os-releaseLook for ID=ubuntu, ID=debian, ID=almalinux, or ID=rocky. The package commands below depend on that result.
Create a non-root admin account
Don't keep using root for the deployment. Create ADMIN_USER, grant sudo access, then test the login in a second terminal before you change SSH access rules.
Ubuntu and Debian
On the VPS as root, create the account and add it to the sudo group.
adduser ADMIN_USER
usermod -aG sudo ADMIN_USERSet a password when prompted. The account should now exist and belong to the sudo group.
Copy your SSH key if you already use one. Run this from your local computer.
ssh-copy-id ADMIN_USER@SERVER_IPIf you don't have a key yet, generate one locally first with ssh-keygen -t ed25519, then copy it. Keep the root session open.
AlmaLinux and Rocky Linux
On the VPS as root, create the account and add it to the wheel group.
useradd -m ADMIN_USER
passwd ADMIN_USER
usermod -aG wheel ADMIN_USERThen copy your SSH key from your local machine.
ssh-copy-id ADMIN_USER@SERVER_IPOpen a second terminal and test the new account before moving on.
ssh ADMIN_USER@SERVER_IP
sudo -v
whoamiYou should see ADMIN_USER from whoami, and sudo -v should succeed after your password prompt. Only after that should you consider disabling root password logins.
Install Docker, Nginx, and utilities
This stack uses Docker for the app and database, with Nginx as the reverse proxy. That keeps the deployment portable if you later move the same stack to another Hostperl VPS or a dedicated server.
Ubuntu and Debian
On the VPS as the non-root sudo user, install the packages you need.
sudo apt update
sudo apt install -y ca-certificates curl gnupg ufw nginx docker.io docker-compose-pluginConfirm the versions.
docker --version
docker compose version
nginx -vAdd your admin user to the docker group so you don't need sudo for every compose command.
sudo usermod -aG docker ADMIN_USERLog out and back in so the group change applies.
AlmaLinux and Rocky Linux
On the VPS as the non-root sudo user, install the packages.
sudo dnf install -y ca-certificates curl nginx firewalld docker docker-compose-pluginEnable Docker and Nginx.
sudo systemctl enable --now docker nginx firewalldCheck the versions.
docker --version
docker compose version
nginx -vIf SELinux is enforcing, keep the default policy and avoid broad disables. This tutorial uses standard volumes and ports, so you should not need to weaken it.
Prepare the app directory and secrets
Create a clean deployment path and store secrets in a root-owned environment file. That keeps API keys out of shell history and makes backups easier to audit.
On the VPS as root, create the application user and directory.
useradd -r -m -d /opt/ai-app -s /usr/sbin/nologin APP_USER
mkdir -p APP_DIRSet ownership to the app account.
chown -R APP_USER:APP_USER APP_DIROn the VPS as root, create the environment file.
cat > APP_DIR/.env <<'EOF'
POSTGRES_DB=aiapp
POSTGRES_USER=aiapp
POSTGRES_PASSWORD=change-this-db-password
APP_PORT=3000
DOMAIN_NAME=ai.example.com
OPENAI_API_KEY=replace-with-your-key
EMBEDDING_MODEL=text-embedding-3-small
LLM_MODEL=gpt-4.1-mini
EOF
chmod 600 APP_DIR/.env
chown APP_USER:APP_USER APP_DIR/.envReplace the sample values with your own. The file should now be readable only by the app account and root.
Build a small private AI app with pgvector
To keep this tutorial easy to verify, use a minimal Node.js app that stores embeddings in PostgreSQL with pgvector. It accepts a prompt, pulls back a small context snippet, and returns a simple answer. That matches the structure many support bots and internal assistants use before they grow into a larger agent stack.
On the VPS as the non-root sudo user, install Git if needed and create the app files.
sudo apt install -y git 2>/dev/null || true
sudo dnf install -y git 2>/dev/null || true
sudo -iu APP_USER
cd /opt/ai-app
mkdir -p appCreate the Node app file.
cat > /opt/ai-app/app/server.js <<'EOF'
const express = require('express');
const { Pool } = require('pg');
const app = express();
app.use(express.json());
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
app.get('/health', (_, res) => res.json({ ok: true }));
app.post('/ask', async (req, res) => {
const q = req.body.q || '';
const { rows } = await pool.query('select content from docs order by id desc limit 3');
res.json({ answer: `Context count: ${rows.length}. Query: ${q}` });
});
app.listen(process.env.APP_PORT || 3000, '0.0.0.0');
EOF
cat > /opt/ai-app/app/package.json <<'EOF'
{
"name": "ai-app",
"version": "1.0.0",
"main": "server.js",
"type": "commonjs",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.21.2",
"pg": "^8.13.1"
}
}
EOFInstall dependencies and check the app starts locally on port 3000.
cd /opt/ai-app/app
npm install
APP_PORT=3000 DATABASE_URL='postgres://aiapp:change-this-db-password@localhost:5432/aiapp' npm startStop it with Ctrl+C after you confirm it starts. In production, you'll run it under systemd or Docker Compose, not in a foreground shell.
Deploy PostgreSQL with pgvector
If your bot or RAG app needs retrieval, PostgreSQL with pgvector is a practical starting point. If you later outgrow it, you can move the same app logic to Qdrant or Redis without changing the external HTTP flow much. For a fuller version of this pattern, see Deploy a Private RAG Stack on VPS in 2026 and RAG app hosting with pgvector and Docker Compose.
On the VPS as the non-root sudo user, create a Compose file that runs PostgreSQL and the app together.
cat > /opt/ai-app/docker-compose.yml <<'EOF'
services:
db:
image: pgvector/pgvector:pg16
environment:
POSTGRES_DB: aiapp
POSTGRES_USER: aiapp
POSTGRES_PASSWORD: change-this-db-password
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U aiapp -d aiapp"]
interval: 10s
timeout: 5s
retries: 5
app:
build: ./app
environment:
APP_PORT: 3000
DATABASE_URL: postgres://aiapp:change-this-db-password@db:5432/aiapp
ports:
- "127.0.0.1:3000:3000"
depends_on:
db:
condition: service_healthy
volumes:
db_data:
EOFCreate a Dockerfile for the app.
cat > /opt/ai-app/app/Dockerfile <<'EOF'
FROM node:22-bookworm-slim
WORKDIR /app
COPY package*.json ./
RUN npm install --omit=dev
COPY server.js ./
EXPOSE 3000
CMD ["npm", "start"]
EOFStart the stack.
cd /opt/ai-app
docker compose up -d --buildCheck the containers.
docker compose ps
docker compose logs --tail=50You should see both services healthy or starting normally. If the app container exits, the logs usually point to a bad secret, a typo in the Compose file, or a missing dependency.
Set up Nginx and TLS
Nginx will expose the app on ports 80 and 443 while the app itself stays bound to 127.0.0.1:3000. That keeps the internal port private and makes future migrations simpler.
On the VPS as the non-root sudo user, install Certbot if it's not already present.
Ubuntu and Debian
sudo apt install -y certbot python3-certbot-nginxAlmaLinux and Rocky Linux
sudo dnf install -y certbot python3-certbot-nginxCreate the Nginx server block.
sudo tee /etc/nginx/conf.d/ai-app.conf >/dev/null <<'EOF'
server {
listen 80;
server_name ai.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
}
EOFTest the config before reloading.
sudo nginx -tIf the syntax is valid, reload Nginx.
sudo systemctl reload nginxOpen the firewall before you test from your browser.
Ubuntu and Debian
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw statusAlmaLinux and Rocky Linux
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-allAfter DNS points DOMAIN_NAME to SERVER_IP, request a certificate.
sudo certbot --nginx -d DOMAIN_NAMEWhen Certbot finishes, it should rewrite the Nginx config for HTTPS and set up renewal.
Run the first smoke tests
Test the app from both the server and your laptop. That catches proxy issues, app crashes, and firewall mistakes before users do.
On the VPS as the non-root sudo user, check the local health endpoint.
curl -i http://127.0.0.1:3000/healthYou should get HTTP/1.1 200 OK and a small JSON body with {"ok":true}.
On your local computer, check the public site.
curl -I https://DOMAIN_NAME
curl -s https://DOMAIN_NAME/healthNow test the retrieval endpoint.
curl -s https://DOMAIN_NAME/ask -H 'Content-Type: application/json' -d '{"q":"What does this private bot know?"}'The response should include the request text and a short answer payload. If the endpoint times out, inspect Nginx and container logs before changing more settings.
Secrets, rate limits, and rollback habits
Private AI app hosting works best when you treat API keys and bot permissions like production credentials, not local dev variables. Keep them in .env files with restrictive permissions, rotate them when staff changes, and avoid committing them to Git.
If your bot talks to customer data, add rate limits at the reverse proxy or application layer. A simple limit per IP protects you from runaway retries, and it keeps token spend visible during a bad integration loop.
Backups matter more here than in a stateless app. Snapshot the VPS through your Hostperl control panel, and test a restore to a staging instance before you assume the database dump is enough. If you need a recovery reference, use VPS backup testing and restore planning.
If a deployment breaks, roll back by pinning the previous Docker image tag or restoring the last known-good Compose file. Keep the old root session open until your new login, firewall, and TLS setup are already verified.
If you're planning private AI app hosting for a support bot, sales assistant, or RAG workflow, Hostperl VPS plans give you the control this kind of deployment needs. You can keep your app, database, logs, and backups in one place, then scale up only when token usage or traffic justifies it.
For teams that want a simpler path, start with a Hostperl VPS and pair it with careful DNS, SSL, and backup hygiene. If you later need more CPU or storage for embeddings and background jobs, moving to a larger managed VPS hosting plan is usually less disruptive than rebuilding around serverless limits.
Troubleshooting the most likely failures
Nginx shows a 502 Bad Gateway. Run sudo journalctl -u nginx -n 50 --no-pager and docker compose logs --tail=50. If the app is not listening on 127.0.0.1:3000, fix the container first, then reload Nginx.
Certbot cannot issue the certificate. Check DNS with dig DOMAIN_NAME +short from your local computer. You should see SERVER_IP. If not, update the record and try again after propagation.
The app cannot connect to PostgreSQL. Run docker compose exec app env | grep DATABASE_URL and docker compose exec db pg_isready -U aiapp -d aiapp. A wrong password or a typo in the Compose file is usually the cause.
The new sudo user cannot log in. Confirm group membership with id ADMIN_USER and inspect /home/ADMIN_USER/.ssh/authorized_keys permissions. The file should be owned by that user and readable only by them.
Why this pattern fits APAC workloads
For New Zealand and broader APAC traffic, a VPS in a nearby region often cuts a noticeable amount of latency compared with a far-away serverless edge. That matters for chat widgets, internal copilots, webhook handlers, and any app that chains multiple API calls before replying.
It also fits audit-friendly deployments. You know where the data lives, which port serves the app, which container runs the database, and what changed between releases. That kind of clarity helps when a client asks why a support bot missed a ticket or why a sales agent spent too many tokens on a simple lookup.
If you want to compare this approach against more managed options, Hostperl's AI hosting buyer's guide and Next.js deployment guide are good follow-ups for teams building the frontend and the retrieval layer together.
Final verification checklist
- Server:
docker compose psshows healthy containers. - Server:
sudo systemctl status nginxreports active. - Server:
ss -tulpn | grep -E ':80|:443|:3000'shows Nginx on 80/443 and the app bound to localhost only. - Client:
curl -I https://DOMAIN_NAMEreturns 200 or 301 to HTTPS. - Client:
curl -s https://DOMAIN_NAME/ask -H 'Content-Type: application/json' -d '{"q":"test"}'returns JSON. - Reboot: after
sudo reboot, bothdocker compose up -dandsudo systemctl status nginxshould recover cleanly.
Once these checks pass, your private AI app hosting setup is ready for real traffic. From here, you can swap in Qdrant, Redis, a Next.js frontend, or a workflow agent without changing the core operational model.
FAQ
Can I replace PostgreSQL with Qdrant or Redis?
Yes. Keep the same Nginx and Compose pattern, then point your app to the new vector store or cache.
Is this better than Vercel or Railway for private AI apps?
For public marketing sites, those platforms are convenient. For private AI app hosting with background jobs, data residency, and tighter cost control, a VPS usually gives you more room to manage the trade-offs.
Do I need a GPU for this setup?
Not for routing, retrieval, or webhook automation. If you later host local inference or a small language model, choose a larger VPS or a dedicated server sized for that workload.
How do I keep token spend under control?
Log prompts, cache repeated lookups, and cap retries. A short system prompt and a smaller model often save more money than people expect.
Can Hostperl help if I migrate from a serverless stack?
Yes. Many teams move from serverless or PaaS to Hostperl when they need simpler backups, more predictable billing, and direct support during DNS or TLS changes.
