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

RAG App Hosting on VPS with pgvector and Docker Compose

By Raman Kumar

Share:

Updated on Jul 28, 2026

RAG App Hosting on VPS with pgvector and Docker Compose

Start with the right RAG app hosting plan

RAG app hosting works best when you size the VPS for retrieval work, not just the web app. For private support bots, sales assistants, and internal search tools, a modest Hostperl VPS is usually enough at the start, especially if you keep embeddings, PostgreSQL, and the app on one machine.

If traffic grows or your vector index expands, move up before the plan feels cramped. Hostperl’s VPS hosting fits this kind of deployment because you can match CPU, RAM, and storage to actual usage.

This tutorial uses a straightforward stack: a Next.js or Node.js app, PostgreSQL with pgvector, and Docker Compose. You get a private setup you can inspect, move, and restore without waiting on a serverless provider to explain the invoice.

If you are comparing options, the context in Hostperl’s AI hosting buyer guide is useful before you build.

What you will deploy

You will deploy a small RAG stack on a fresh VPS:

  • APP_USER: non-root Linux account for the app
  • APP_DIR: application directory, for example /opt/ragbot
  • DOMAIN_NAME: public hostname, for example rag.example.com
  • APP_PORT: local app port, for example 3000
  • DB_NAME, DB_USER, DB_PASSWORD: PostgreSQL credentials

You’ll also set up Docker Compose, Nginx, TLS, and a small smoke test so you can verify the app after reboot. If your team needs a private deployment with tighter security and cost control, this is the same pattern covered in Private AI app hosting on VPS.

1) Connect to the VPS and check the operating system

On your local computer, connect as root. Replace 203.0.113.10 with your server IP.

ssh root@203.0.113.10

If your provider gives you a default user instead of root, use that account name in the same command. Keep this first session open until the new sudo user is working.

On the VPS as root, confirm the OS before installing packages.

cat /etc/os-release

You should see either Ubuntu/Debian or AlmaLinux/Rocky Linux. The next steps split by family.

2) Create a non-root admin user

Ubuntu and Debian

adduser adminuser
usermod -aG sudo adminuser

Replace adminuser with your own admin account. The first command creates the user and prompts for a password. The second adds sudo access.

You should be able to run sudo -v later without errors.

AlmaLinux and Rocky Linux

useradd -m adminuser
passwd adminuser
usermod -aG wheel adminuser

The wheel group gives sudo rights on these systems. Set a password only if you need password login; SSH keys are better for day-to-day use.

Now add your SSH key.

mkdir -p /home/adminuser/.ssh
chmod 700 /home/adminuser/.ssh
cp /root/.ssh/authorized_keys /home/adminuser/.ssh/authorized_keys
chown -R adminuser:adminuser /home/adminuser/.ssh
chmod 600 /home/adminuser/.ssh/authorized_keys

These commands copy the current authorized key into the new account for testing. If you manage keys from your laptop, append the public key to authorized_keys instead.

Open a second terminal and test the login before you change root access.

On your local computer, verify the new account.

ssh adminuser@203.0.113.10

On the VPS as the non-root sudo user, confirm sudo works.

sudo -v
whoami

You should see no sudo error, and whoami should return adminuser. Only after this test should you consider disabling root password login later.

3) Install Docker, Compose, and Nginx

Ubuntu and Debian

sudo apt update
sudo apt install -y ca-certificates curl gnupg nginx docker.io docker-compose-plugin
sudo systemctl enable --now nginx docker

AlmaLinux and Rocky Linux

sudo dnf install -y dnf-plugins-core curl ca-certificates nginx docker-compose-plugin docker-cli
sudo systemctl enable --now nginx docker

If Docker is not available from your default repository on your RHEL-family system, install the supported Docker repository package first, then repeat the install command. Run a quick version check so you know the binaries are present.

docker --version
docker compose version
nginx -v

At this point you have the tools needed for a private AI app stack. If you want a broader deployment comparison, Hostperl’s RAG, bots, and webhooks deployment guide covers the same hosting pattern from a customer-operations angle.

4) Create the application directory and secrets file

On the VPS as the non-root sudo user, create the app directory and a file for environment variables.

sudo mkdir -p /opt/ragbot
sudo chown -R adminuser:adminuser /opt/ragbot
cd /opt/ragbot
pwd
mkdir -p app

Now create .env. Replace the sample values with your real passwords and domain.

cat > /opt/ragbot/.env <<'EOF'
DOMAIN_NAME=rag.example.com
APP_PORT=3000
DB_NAME=ragdb
DB_USER=raguser
DB_PASSWORD=change-this-long-password
POSTGRES_PASSWORD=change-this-stronger-password
EOF
chmod 600 /opt/ragbot/.env

That file should stay readable only by the app owner. It keeps the deployment easier to audit and keeps secrets out of shell history.

5) Add the Docker Compose stack

Use PostgreSQL with pgvector plus a small web app container. The app below is a simple Node.js example that stores and reads documents from Postgres; you can replace it with Next.js, NestJS, or FastAPI later.

If your team is comparing deployment platforms, Hostperl’s private AI hosting cost and latency guide explains why this kind of self-managed stack stays predictable.

On the VPS as the non-root sudo user, create docker-compose.yml.

cat > /opt/ragbot/docker-compose.yml <<'EOF'
services:
  db:
    image: pgvector/pgvector:pg16
    container_name: ragbot-db
    restart: unless-stopped
    env_file: .env
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
      interval: 10s
      timeout: 5s
      retries: 5

  app:
    image: node:22-alpine
    container_name: ragbot-app
    restart: unless-stopped
    working_dir: /app
    command: sh -c "npm install express pg && node server.js"
    env_file: .env
    environment:
      PORT: ${APP_PORT}
      DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
    volumes:
      - ./app:/app
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "127.0.0.1:${APP_PORT}:${APP_PORT}"

volumes:
  db_data:
EOF

The loopback bind keeps the app private behind Nginx. That is the right default for support bots and sales workflows where you do not want the container exposed directly on the public IP.

6) Add a small RAG-style Node.js app

Create a minimal server that stores a document and returns a search result. It is not a full vector pipeline, but it proves the hosting path, database connection, and deployment flow.

On the VPS as the non-root sudo user, create /opt/ragbot/app/server.js.

cat > /opt/ragbot/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 });
const port = process.env.PORT || 3000;

(async () => {
  await pool.query('CREATE EXTENSION IF NOT EXISTS vector');
  await pool.query(`CREATE TABLE IF NOT EXISTS docs (
    id SERIAL PRIMARY KEY,
    title TEXT NOT NULL,
    body TEXT NOT NULL
  )`);
})();

app.get('/healthz', async (_req, res) => {
  try {
    await pool.query('SELECT 1');
    res.json({ ok: true });
  } catch (err) {
    res.status(500).json({ ok: false, error: err.message });
  }
});

app.post('/docs', async (req, res) => {
  const { title, body } = req.body;
  const result = await pool.query('INSERT INTO docs(title, body) VALUES ($1, $2) RETURNING id', [title, body]);
  res.json({ id: result.rows[0].id });
});

app.get('/docs', async (_req, res) => {
  const result = await pool.query('SELECT id, title, body FROM docs ORDER BY id DESC LIMIT 10');
  res.json(result.rows);
});

app.listen(port, () => console.log(`RAG app listening on ${port}`));
EOF

OpenAI, local embeddings, pgvector, or Qdrant can sit behind this same pattern. For a deeper vector-db deployment example, link this guide with Hostperl’s pgvector and Docker Compose tutorial.

7) Start the stack and confirm it runs

On the VPS as the non-root sudo user, bring up the containers.

cd /opt/ragbot
docker compose up -d
docker compose ps

You should see both containers in a running state. If the app exits, inspect logs immediately.

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

Next, test the app from the VPS itself.

curl -s http://127.0.0.1:3000/healthz
curl -s -X POST http://127.0.0.1:3000/docs -H 'Content-Type: application/json' -d '{"title":"Pricing note","body":"Reply with the 2026 renewal price and next steps."}'
curl -s http://127.0.0.1:3000/docs

You should get {"ok":true}, then a document ID, then the stored document list. That confirms the container, database, and app path are working before you add the reverse proxy.

8) Put Nginx in front of the app

On the VPS as the non-root sudo user, create an Nginx site file. Replace rag.example.com with your real domain.

sudo tee /etc/nginx/conf.d/ragbot.conf > /dev/null <<'EOF'
server {
    listen 80;
    server_name rag.example.com;

    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;
    }
}
EOF
sudo nginx -t
sudo systemctl reload nginx

The nginx -t check should report syntax is ok. Only reload after that. If you are new to reverse proxies, Hostperl’s Next.js on VPS deployment guide uses the same front-end pattern.

9) Open the firewall and issue TLS

Ubuntu and Debian

sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status

AlmaLinux and Rocky Linux

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

Then request a certificate. Make sure DNS already points rag.example.com to the VPS IP.

sudo apt install -y certbot python3-certbot-nginx || true
sudo certbot --nginx -d rag.example.com

On AlmaLinux or Rocky, install the matching Certbot package from EPEL or your preferred repository first, then run the same Certbot command. When it finishes, open https://rag.example.com in a browser. The certificate should be valid and the app should load through Nginx.

10) Verify reboot persistence, logs, and rollback path

Restart the VPS or at least test the service states after a reload. You want to know the stack comes back on its own.

docker compose ps
systemctl status nginx --no-pager
journalctl -u nginx -n 20 --no-pager

From your local computer, run a final check against the public domain.

curl -I https://rag.example.com
curl -s https://rag.example.com/healthz

If you need to roll back after a bad app push, the cleanest move is to update the files in /opt/ragbot/app, then run docker compose up -d again. For bigger changes, keep a copy of the previous docker-compose.yml and .env so you can restore the last known good state quickly.

Troubleshooting the usual failures

Nginx returns 502

Check whether the app container is listening on the local port.

docker compose logs --tail=50 app
curl -s http://127.0.0.1:3000/healthz

If the health check fails, fix the container error first. If the app works locally, confirm Nginx points to 127.0.0.1:3000.

PostgreSQL extension error

If CREATE EXTENSION IF NOT EXISTS vector fails, verify the image tag is pgvector-enabled.

docker compose logs --tail=50 db
docker image ls | grep pgvector

Replace the database image with a pgvector image if you used a plain Postgres tag.

Certbot cannot issue the certificate

DNS or firewall is usually the problem.

dig +short rag.example.com
sudo ss -tlnp | grep ':80\|:443'

If the IP is wrong, fix the DNS record and wait for propagation. If port 80 is closed, re-open the firewall rule before retrying Certbot.

If you are launching a private RAG app, Hostperl gives you the control you need without making the setup heavier than it should be. Start with a right-sized Hostperl VPS, then follow the same deployment pattern you used here for support bots, sales assistants, or internal search tools.

For teams that want to compare deployment styles before moving production traffic, the AI hosting buyer guide is the fastest way to sanity-check your next step.

FAQ

Should I use pgvector, Qdrant, or Chroma?

Use pgvector if you already rely on PostgreSQL and want one backup path. Qdrant fits larger vector-heavy workloads. Chroma is fine for prototypes, but most production teams prefer clearer operational control.

Can I host a private AI app on one small VPS?

Yes, if traffic is light and your vector index is modest. Start small, watch memory and disk use, then scale before the host hits swap.

How do I keep secrets out of code?

Store API keys and database passwords in .env with strict permissions, or move them into a secrets manager later. Do not hardcode keys in the app files.

What should I watch for with APAC users?

Choose a datacenter close to your users if the workflow is latency-sensitive. For New Zealand and wider APAC teams, a nearby VPS usually feels faster than a distant serverless region.

How do I control AI hosting cost?

Cache repeated prompts, keep embeddings local, and use a small model for routing or classification. The cheapest request is the one you do not send to an external API.