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

Host AI Apps on VPS with pgvector and Docker Compose

By Raman Kumar

Share:

Updated on Jul 30, 2026

Host AI Apps on VPS with pgvector and Docker Compose

Why this setup fits private AI apps

If you want to host AI apps on VPS for support bots, sales assistants, or RAG workflows, a compact private stack is often simpler than stitching together serverless services. You keep the app, database, vector search, and background jobs in one place, which makes data residency, audit trails, and debugging much easier. For teams that want that control without building everything from scratch, a Hostperl VPS is a practical starting point.

This tutorial uses Docker Compose, PostgreSQL with pgvector, and a Node.js app that exposes a simple /ask endpoint. You can adapt the same pattern for OpenClaw, Next.js, NestJS, FastAPI, Qdrant, or Redis-backed queues. If you are comparing private hosting models first, the related guide on private AI app hosting with RAG and security is a useful companion.

What you will deploy

We will build a small but real AI service:

  • PostgreSQL 16 with pgvector for embeddings
  • A Node.js API for chat and retrieval requests
  • Redis for caching and queue work
  • Nginx as the public reverse proxy
  • Let's Encrypt TLS for the domain

Use these placeholders throughout the tutorial:

  • SERVER_IP = your VPS IP, for example 203.0.113.10
  • DOMAIN_NAME = your domain, for example ai.example.nz
  • APP_USER = the non-root admin user, for example hostperl
  • APP_DIR = the app directory, for example /opt/ai-stack
  • APP_PORT = the internal app port, for example 3000

1) Connect to the VPS and confirm the OS

From your local machine, SSH into the server as root. If your provider gave you a default non-root account, use that instead.

ssh root@203.0.113.10

Once connected, confirm the operating system before installing anything else.

cat /etc/os-release

You should see either Ubuntu/Debian or AlmaLinux/Rocky Linux. The remaining steps are split where commands differ.

2) Create a non-root admin user

Keep the root session open until the new login works. Open a second terminal and complete the steps below on the VPS as root.

Ubuntu and Debian

adduser hostperl
usermod -aG sudo hostperl
mkdir -p /home/hostperl/.ssh
chmod 700 /home/hostperl/.ssh
cat /root/.ssh/authorized_keys > /home/hostperl/.ssh/authorized_keys
chown -R hostperl:hostperl /home/hostperl/.ssh
chmod 600 /home/hostperl/.ssh/authorized_keys

This creates hostperl, grants sudo, and copies the existing SSH key so you can test the new account safely.

AlmaLinux and Rocky Linux

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

On these systems, sudo access comes from the wheel group. After this, open a second terminal and test the new login.

ssh hostperl@203.0.113.10
sudo -v
whoami
pwd

You should land on the server as hostperl, and sudo -v should prompt for your password or succeed. Only then should you tighten root access.

3) Install Docker, Compose, Nginx, and Git

On the VPS as the non-root sudo user, install the base packages. Hostperl customers usually choose this path because it keeps deployment state readable and easier to recover after a support call or migration.

Ubuntu and Debian

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

Check versions so you know what you are running.

nginx -v
docker --version
docker compose version

AlmaLinux and Rocky Linux

sudo dnf install -y dnf-utils ca-certificates curl git nginx docker docker-compose-plugin
sudo systemctl enable --now nginx docker

Then confirm the services are active.

nginx -v
docker --version
docker compose version
sudo systemctl status nginx --no-pager
sudo systemctl status docker --no-pager

4) Open the firewall before you close anything down

Open SSH, HTTP, and HTTPS first. Do not remove your working root session until the new user and firewall are verified.

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

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 you later disable password SSH login, test key-based access in a second terminal first.

5) Create the application directory and Compose stack

On the VPS as hostperl, create the app directory and move into it. This keeps the deployment in one place, which helps with backups and rollback.

sudo mkdir -p /opt/ai-stack
sudo chown -R hostperl:hostperl /opt/ai-stack
cd /opt/ai-stack
pwd

Now create the Compose file.

nano docker-compose.yml

Paste this file content exactly, replacing only the sample passwords and hostnames.

services:
  postgres:
    image: pgvector/pgvector:pg16
    container_name: ai_postgres
    environment:
      POSTGRES_DB: aiapp
      POSTGRES_USER: aiapp
      POSTGRES_PASSWORD: change-this-db-password
    volumes:
      - pgdata:/var/lib/postgresql/data
    restart: unless-stopped

  redis:
    image: redis:7.4-alpine
    container_name: ai_redis
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - redisdata:/data
    restart: unless-stopped

  app:
    image: node:22-alpine
    container_name: ai_app
    working_dir: /app
    command: sh -c "npm install && node server.js"
    env_file:
      - .env
    volumes:
      - ./:/app
    ports:
      - "127.0.0.1:3000:3000"
    depends_on:
      - postgres
      - redis
    restart: unless-stopped

volumes:
  pgdata:
  redisdata:

Save and exit with Ctrl+O, Enter, then Ctrl+X.

6) Add secure environment variables

Create a private .env file. This is where you keep API keys for model providers, webhook secrets, and database settings.

nano .env

Use this sample.

NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://aiapp:change-this-db-password@postgres:5432/aiapp
REDIS_URL=redis://redis:6379
OPENAI_API_KEY=replace-with-your-key
EMBEDDING_MODEL=text-embedding-3-small
CHAT_MODEL=gpt-4.1-mini
WEBHOOK_SECRET=replace-with-a-long-random-string

Protect the file immediately after saving.

chmod 600 .env

For audit-friendly deployments, this file stays on the VPS and is not committed to Git.

7) Create the Node.js app

The next file gives you a working retrieval endpoint and a health check. It is intentionally small so you can swap in Next.js, NestJS, FastAPI, or OpenClaw later.

nano server.js
const http = require('http');
const { Pool } = require('pg');
const Redis = require('ioredis');

const port = process.env.PORT || 3000;
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const redis = new Redis(process.env.REDIS_URL);

async function init() {
  await pool.query('CREATE EXTENSION IF NOT EXISTS vector;');
  await pool.query(`CREATE TABLE IF NOT EXISTS docs (
    id bigserial PRIMARY KEY,
    title text NOT NULL,
    body text NOT NULL,
    embedding vector(3)
  )`);
  await pool.query(`INSERT INTO docs (title, body, embedding)
    VALUES ('Support hours', 'We answer billing and migration tickets 24/7 from NZ/APAC coverage windows.', '[0.1,0.2,0.3]')
    ON CONFLICT DO NOTHING`);
}

function json(res, code, data) {
  res.writeHead(code, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify(data));
}

const server = http.createServer(async (req, res) => {
  if (req.url === '/health') {
    return json(res, 200, { ok: true });
  }

  if (req.url === '/ask') {
    const cached = await redis.get('answer:sample');
    if (cached) return json(res, 200, { source: 'cache', answer: cached });

    const result = await pool.query('SELECT title, body FROM docs LIMIT 1');
    const answer = `Matched: ${result.rows[0].title} — ${result.rows[0].body}`;
    await redis.set('answer:sample', answer, 'EX', 300);
    return json(res, 200, { source: 'db', answer });
  }

  json(res, 404, { error: 'not found' });
});

init()
  .then(() => server.listen(port, '0.0.0.0'))
  .then(() => console.log(`Listening on ${port}`))
  .catch(err => {
    console.error(err);
    process.exit(1);
  });

Save and exit, then create the package manifest.

nano package.json
{
  "name": "ai-stack",
  "version": "1.0.0",
  "main": "server.js",
  "type": "commonjs",
  "dependencies": {
    "ioredis": "^5.4.1",
    "pg": "^8.13.0"
  }
}

Install the app dependencies inside the container later, so you do not need Node directly on the VPS.

8) Start the stack and check the containers

Bring everything up on the VPS as hostperl.

docker compose up -d
docker compose ps

You should see three running containers. If the app exits, inspect the logs next.

docker compose logs --tail=100 app

A clean startup should end with Listening on 3000.

9) Put Nginx in front of the app

Create a reverse proxy so visitors reach the app on standard web ports. This is the point where Hostperl customers often finish a migration from a serverless prototype to a private VPS deployment.

sudo nano /etc/nginx/conf.d/ai-stack.conf
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;
    }
}

Replace DOMAIN_NAME with your real domain. Test syntax before you reload.

sudo nginx -t
sudo systemctl reload nginx

You should see syntax is ok and test is successful.

10) Add TLS with Let's Encrypt

Point your DNS A record to SERVER_IP first. Then install the certificate tool and request a certificate.

Ubuntu and Debian

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

AlmaLinux and Rocky Linux

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

Choose the redirect option so HTTP goes to HTTPS. Then test renewal.

sudo certbot renew --dry-run

11) Test the app from the server and your laptop

On the VPS, confirm the app responds locally.

curl -s http://127.0.0.1:3000/health | jq
curl -s http://127.0.0.1:3000/ask | jq

From your local computer, test the public site.

curl -s https://DOMAIN_NAME/health | jq
curl -s https://DOMAIN_NAME/ask | jq

Expected result: {"ok":true} on health and a JSON answer for /ask. If you see cache hits, Redis is working too.

12) Check persistence, logs, and restart behavior

Restart the host and confirm the services come back automatically.

sudo reboot

After reconnecting, run these checks on the VPS.

docker compose ps
sudo systemctl status nginx --no-pager
sudo systemctl status docker --no-pager
curl -s https://DOMAIN_NAME/health | jq

If everything returns cleanly after reboot, your deployment is ready for real customer traffic, support bots, or internal workflow agents.

Troubleshooting the most likely failures

  • Nginx returns 502. Run docker compose logs --tail=100 app. If the app crashed on startup, fix the missing package or bad environment variable, then run docker compose up -d again.
  • Database extension errors. Run docker compose exec postgres psql -U aiapp -d aiapp -c '\dx'. If vector is missing, confirm you used the pgvector image, not stock Postgres.
  • TLS issuance fails. Run sudo nginx -t and check that DNS for DOMAIN_NAME points to SERVER_IP. Then rerun sudo certbot --nginx -d DOMAIN_NAME.
  • SSH lockout risk. Keep the root session open until a second terminal proves that hostperl can log in and use sudo.

Where this pattern fits next

Once this stack is live, you can swap the sample Node app for a real RAG pipeline, a Next.js admin panel, or a private sales bot. Many Hostperl customers start here, then move into dedicated resources or a larger managed VPS hosting plan when token volume, queue depth, or vector size grows.

If you are comparing deployment models before your next rollout, the article on Vercel alternatives and private bots explains why a private VPS often wins on control, cost visibility, and data residency. For retrieval-heavy builds, the tutorial on RAG app hosting with pgvector on a Hostperl VPS is the next logical step.

Hostperl is a strong fit if you want to host AI apps on VPS with support you can actually reach, especially for private bots, RAG services, and regional workloads. If you need a right-sized setup with room for PostgreSQL, Redis, Docker Compose, and TLS, start with Hostperl VPS hosting and scale as traffic grows.

For teams comparing private deployments with a more controlled footprint, managed VPS hosting gives you a practical base for migrations, backups, and launch-day fixes.

FAQ

Can I use this setup for Next.js or FastAPI?
Yes. Keep Nginx, TLS, PostgreSQL, and Redis the same, then replace the app container with your framework image or process.

Is pgvector enough for a small RAG app?
For many customer-service bots and internal assistants, yes. It is easy to back up, works well with Postgres, and keeps your stack simple.

How do I keep secrets safe?
Store them in .env, protect the file with chmod 600, and avoid committing it to Git. For larger teams, move secrets into your deployment pipeline or a vault tool.

What if I later need Qdrant or Chroma?
Add another container to Compose and point your retrieval code at the new vector service. The reverse proxy and TLS steps do not change.

How does this help with APAC latency?
A VPS closer to your users usually cuts response time and keeps support agents and internal workflows feeling faster, especially for New Zealand and nearby APAC traffic.