AI App Hosting on VPS for RAG and Private Bots

Start with the hosting choice that fits the workload
AI app hosting is usually a VPS decision, not a framework decision. If your bot handles support replies, sales qualification, RAG lookups, or internal workflows, you need predictable CPU, fixed memory, and control over where the data lives. That is why many teams start on a Hostperl VPS instead of pushing everything onto serverless platforms with tighter limits and less operational visibility.
This guide shows you how to deploy a private AI app with Docker Compose, Nginx, HTTPS, environment files, and a simple retrieval stack. It is written for the person who needs the app live, stable, and easy to support when a client asks what changed.
If you want to compare platforms first, Hostperl also has a useful rundown in Private AI Hosting for Bots, RAG, and Next.js Apps. This tutorial focuses on the build and deployment steps.
What you need before you connect
Use these placeholders throughout the guide: SERVER_IP is your VPS address, DOMAIN_NAME is the hostname you will point at the server, ADMIN_USER is your sudo user, APP_USER runs the app, APP_DIR is the deploy directory, and APP_PORT is the internal app port. Example values: 203.0.113.10, ai.example.com, deploy, aiapp, /srv/aiapp, and 3000.
From your local computer, connect with SSH. If your provider gives you root access first, use the root example. If you already have a non-root login, use that instead.
ssh root@SERVER_IP
# or
ssh ADMIN_USER@SERVER_IPAfter login, confirm the operating system before you install anything.
cat /etc/os-releaseYou should see either Ubuntu/Debian or AlmaLinux/Rocky Linux. The package commands below are split accordingly.
Create a non-root admin account safely
Do not disable root access until the new account works in a second terminal. Keep the first SSH session open.
Ubuntu and Debian
On the VPS as root, create the admin user, add sudo access, and set up SSH keys.
adduser ADMIN_USER
usermod -aG sudo ADMIN_USER
mkdir -p /home/ADMIN_USER/.ssh
chmod 700 /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 600 /home/ADMIN_USER/.ssh/authorized_keysIf you do not already have a key on root, copy your public key from your local computer instead:
ssh-copy-id ADMIN_USER@SERVER_IPOpen a second terminal on your local computer and test the new account.
ssh ADMIN_USER@SERVER_IP
sudo -vIf sudo works, you can later disable password login or root login. For now, leave them unchanged until your deployment is verified.
AlmaLinux and Rocky Linux
On the VPS as root, create the admin user, add wheel access, and prepare SSH keys.
useradd -m -s /bin/bash ADMIN_USER
passwd ADMIN_USER
usermod -aG wheel ADMIN_USER
mkdir -p /home/ADMIN_USER/.ssh
chmod 700 /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 600 /home/ADMIN_USER/.ssh/authorized_keysTest the new login from a second terminal before making any SSH changes.
ssh ADMIN_USER@SERVER_IP
sudo -vInstall Docker, Nginx, and the basic runtime
The example stack uses Docker Compose because it keeps app settings, worker jobs, and vector services together. That is a practical fit for AI app hosting, especially if you are replacing a fragile setup on Vercel, Render, or Railway with something you can actually troubleshoot.
Ubuntu and Debian
sudo apt update
sudo apt install -y ca-certificates curl gnupg ufw nginxAdd Docker’s repository, then install the engine and Compose plugin.
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
. /etc/os-release && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/$ID $VERSION_CODENAME stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker ADMIN_USERCheck versions.
docker --version
docker compose version
nginx -vAlmaLinux and Rocky Linux
sudo dnf install -y dnf-plugins-core firewalld nginx
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker firewalld nginx
sudo usermod -aG wheel ADMIN_USERVerify the installed versions and running services.
docker --version
docker compose version
systemctl status docker --no-pager
systemctl status nginx --no-pagerBuild the app directory and environment files
Now create the deployment directory. The app below is a small Node.js API that exposes a chat endpoint, a health check, and a retrieval stub you can later connect to pgvector, Qdrant, Chroma, or Redis.
sudo mkdir -p /srv/aiapp/app
sudo chown -R ADMIN_USER:ADMIN_USER /srv/aiappOn the VPS as the non-root sudo user, create the environment file. Keep real secrets out of your shell history.
cd /srv/aiapp
cat > .env <<'EOF'
NODE_ENV=production
APP_PORT=3000
DOMAIN_NAME=ai.example.com
OPENAI_API_KEY=replace-with-your-key
DATABASE_URL=postgresql://aiapp:strongpassword@db:5432/aiapp
EOF
chmod 600 .envThat file should be readable only by your deployment user. If you later add bot permissions, rate limits, or audit logging, keep those values in the same file or in Docker secrets.
Deploy the application with Docker Compose
Create a minimal app, a PostgreSQL database with pgvector, and an Nginx reverse proxy. This is a common pattern for private AI hosting because it keeps embeddings close to the app and avoids unnecessary third-party hops.
On the VPS as the non-root sudo user, create the application file first.
cat > /srv/aiapp/app/server.js <<'EOF'
const express = require('express');
const app = express();
app.use(express.json({ limit: '1mb' }));
app.get('/health', (_req, res) => res.json({ ok: true }));
app.post('/chat', (req, res) => {
const message = req.body.message || '';
res.json({ reply: `Received: ${message}` });
});
app.listen(process.env.APP_PORT || 3000, '0.0.0.0');
EOFThen create the Dockerfile.
cat > /srv/aiapp/app/Dockerfile <<'EOF'
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
EOFCreate the package files.
cat > /srv/aiapp/app/package.json <<'EOF'
{
"name": "aiapp",
"version": "1.0.0",
"main": "server.js",
"type": "commonjs",
"dependencies": {
"express": "^4.21.2"
}
}
EOF
cd /srv/aiapp/app
npm install --package-lock-onlyNow create the Compose file. It includes PostgreSQL with the pgvector image tag and a local app service.
cat > /srv/aiapp/docker-compose.yml <<'EOF'
services:
app:
build: ./app
env_file: .env
ports:
- "127.0.0.1:3000:3000"
depends_on:
- db
restart: unless-stopped
db:
image: pgvector/pgvector:pg16
environment:
POSTGRES_USER: aiapp
POSTGRES_PASSWORD: strongpassword
POSTGRES_DB: aiapp
volumes:
- dbdata:/var/lib/postgresql/data
restart: unless-stopped
volumes:
dbdata:
EOFBring the stack up and check that both containers are running.
cd /srv/aiapp
docker compose up -d --build
docker compose psYou should see the app and database containers in a healthy or running state.
Add Nginx and HTTPS
Proxying through Nginx gives you a stable front door, simple TLS, and a place to enforce headers and rate limits. It also keeps your app private on 127.0.0.1, which matters for internal tools and APAC customer data.
Ubuntu and Debian
sudo nano /etc/nginx/sites-available/aiappPaste this config, then save and exit.
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;
}
}
sudo ln -s /etc/nginx/sites-available/aiapp /etc/nginx/sites-enabled/aiapp
sudo nginx -t
sudo systemctl reload nginxInstall Certbot and issue a certificate.
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d ai.example.comAlmaLinux and Rocky Linux
sudo nano /etc/nginx/conf.d/aiapp.confUse this server block, then save and exit.
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;
}
}
sudo nginx -t
sudo systemctl reload nginx
sudo dnf install -y certbot python3-certbot-nginx
sudo certbot --nginx -d ai.example.comIf SELinux blocks the proxy on AlmaLinux or Rocky, confirm the boolean is set.
sudo setsebool -P httpd_can_network_connect 1Verify the app from server and client
First check the local service on the VPS. You should get JSON back from the health endpoint.
curl -s http://127.0.0.1:3000/health | jqThen test the public domain from your local computer.
curl -I https://ai.example.com
curl -s https://ai.example.com/health | jq
curl -s -X POST https://ai.example.com/chat -H 'Content-Type: application/json' -d '{"message":"hello"}' | jqCheck the listening ports and container logs on the VPS if anything looks off.
sudo ss -tulpn | grep -E '(:80|:443|:3000)'
docker compose logs --tail=50 app
docker compose logs --tail=50 dbCost control, backups, and rollout discipline
For AI hosting, costs usually creep up in three places: tokens, vector storage, and retries. Keep a simple usage table in your app, cache repeated prompt results where the answer should not change, and use a right-sized VPS rather than paying for idle serverless bursts. Hostperl’s VPS line is a better fit when you want a fixed monthly bill and room for a queue worker or retrieval service alongside the API.
For data safety, back up the Compose directory and the database volume. Hostperl’s VPS backup testing guide is a useful companion if you need a restore drill rather than just a backup job.
To prepare a rollback, keep the previous image tag and Compose file. If a release breaks webhooks, bot replies, or retrieval latency, you can revert with a single docker compose up -d --build after restoring the old files.
If you are deploying a support bot, sales assistant, or private RAG app for a client, Hostperl gives you the control you need without a serverless billing surprise. A Hostperl VPS is a practical fit for Docker Compose, Nginx, and regional deployments that need predictable latency and cleaner data handling.
For teams comparing deployment models, the planning notes in AI app hosting for bots, RAG, and private workloads will help you decide what belongs on a VPS and what should stay managed elsewhere.
Troubleshooting the common failures
502 Bad Gateway: Run docker compose ps and docker compose logs --tail=50 app. If the app exited, rebuild with docker compose up -d --build.
Nginx syntax errors: Use nginx -t. Fix the file it names, then reload Nginx again.
SSL renewal problems: Check sudo certbot renew --dry-run. If the challenge fails, confirm your DNS record points to the VPS and port 80 is open.
Database connection errors: Run docker compose logs --tail=50 db. If PostgreSQL is still initializing, wait a minute and retry. If the password is wrong, correct .env and the Compose file together so they match.
FAQ
Can I use this for Next.js, FastAPI, or NestJS? Yes. Keep the same Nginx, firewall, SSL, and Compose flow. Replace the app container and port.
Is this better than Vercel for private AI workloads? Usually yes when you need fixed infrastructure, local databases, or tighter control over data residency.
Should I add Qdrant or Redis? Add them only when retrieval, caching, or workflow queues justify the extra service. Start small.
How do I keep secrets out of the repository? Store them in .env with mode 600, or move them to Docker secrets if the deployment grows.
What should I monitor first? Service uptime, container restarts, response time on /health, and whether your chat or webhook endpoint returns 2xx responses.
Once this stack is stable, you can add pgvector retrieval, a worker queue, and better bot controls without changing the basic hosting model. That is the main advantage of building on managed VPS hosting: you keep the app private, supportable, and easy to grow.
