Run a Private AI Support Bot on VPS in 2026

Start with the right deployment shape
A private AI support bot on VPS gives you control over latency, data residency, and support visibility. It also keeps the stack simple enough that your team can maintain it without guessing what broke.
If you are choosing between hosted serverless tools and a VPS, read Hostperl’s AI hosting for support bots and RAG apps on VPS first. For many teams, a small Hostperl VPS is the cleaner starting point: one box for the app, one database or vector store, and one support path when something needs attention.
Use this tutorial as a fresh-server runbook. We will install Docker, run a simple private AI support bot stack with Node.js, Redis, and Postgres with pgvector, place it behind Nginx, add SSL, and verify that webhooks and background jobs still work after a reboot.
- SERVER_IP: your VPS public IP, such as
203.0.113.10 - DOMAIN_NAME: the bot hostname, such as
bot.example.com - ADMIN_USER: your sudo account, such as
hostperladmin - APP_USER: the service account for the bot, such as
aiapp - APP_DIR: the app directory, such as
/opt/ai-support-bot - APP_PORT: the app port, such as
3000
Connect to the VPS and identify the OS
Run the first SSH command from your local computer. If Hostperl gave you a non-root login, use that instead of root.
ssh root@203.0.113.10Or, if your server already has a default admin user:
ssh ADMIN_USER@203.0.113.10After you log in, detect the operating system before you install anything.
cat /etc/os-releaseYou should see either Ubuntu/Debian fields or AlmaLinux/Rocky fields. The commands below are split for that reason. Do not mix them.
Create a non-root admin account and keep root open
Use the root session only long enough to create your normal login. Keep this root terminal open until the new account works in a second terminal.
Ubuntu and Debian
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 2>/dev/null || true
chown -R ADMIN_USER:ADMIN_USER /home/ADMIN_USER/.ssh
chmod 600 /home/ADMIN_USER/.ssh/authorized_keysThis creates the admin user, grants sudo access, and copies an existing SSH key if one is already present. If you do not have an authorized key yet, add your public key manually before proceeding.
AlmaLinux and Rocky Linux
useradd -m 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 2>/dev/null || true
chown -R ADMIN_USER:ADMIN_USER /home/ADMIN_USER/.ssh
chmod 600 /home/ADMIN_USER/.ssh/authorized_keysHere, wheel is the sudo-equivalent group. If you need passwordless SSH only, set the password now but expect to use keys for daily login.
Open a second terminal on your local computer and test the new account before changing root access.
ssh ADMIN_USER@203.0.113.10
sudo -v
whoamiYou want to see ADMIN_USER and a successful sudo credential check. Leave the original root session open until this works.
Install Docker, Nginx, and support tools
This stack uses Docker Compose for the bot, Nginx as the reverse proxy, and a small set of system packages for firewall and certificate work. If you need a broader reference for VPS app setup, Hostperl’s Next.js deployment tutorial shows the same host-side pattern used here.
Ubuntu and Debian
sudo apt update
sudo apt install -y ca-certificates curl gnupg nginx ufw
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release; echo $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-compose-plugin
sudo systemctl enable --now nginx dockerAfter this, confirm the versions and that both services are running.
nginx -v
docker --version
docker compose version
systemctl status nginx --no-pager
systemctl status docker --no-pagerAlmaLinux and Rocky Linux
sudo dnf install -y dnf-plugins-core ca-certificates curl nginx firewalld
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-compose-plugin
sudo systemctl enable --now nginx docker firewalldConfirm the installation and active services.
nginx -v
docker --version
docker compose version
systemctl status nginx --no-pager
systemctl status docker --no-pagerIf SELinux is enforcing, keep it enabled. We will use standard paths and an HTTP proxy, which avoids risky policy changes.
Open the firewall before publishing the app
Add the new rules first, then test them. Do not remove SSH access until you know the new login works.
Ubuntu and Debian with UFW
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw --force enable
sudo ufw status verboseYou should see SSH and Nginx allowed. If you are already using custom SSH ports, add that port before enabling UFW.
AlmaLinux and Rocky Linux with 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-allThat opens port 80 and 443 for the bot, plus SSH for administration.
Prepare the app directory, secrets, and Docker Compose stack
We will build a small stack that looks like a real customer-service deployment: a Node.js bot API, Postgres with pgvector for retrieval, Redis for jobs and rate limiting, and Nginx in front. For an alternate vector-store approach, Hostperl’s pgvector and Docker Compose guide covers the database side in more detail.
Run these commands as your non-root sudo user.
sudo mkdir -p /opt/ai-support-bot
sudo chown -R ADMIN_USER:ADMIN_USER /opt/ai-support-bot
cd /opt/ai-support-bot
pwdNow create the environment file. Replace the sample secrets with your own values.
cat > .env <<'EOF'
APP_PORT=3000
DOMAIN_NAME=bot.example.com
POSTGRES_DB=aisupport
POSTGRES_USER=aisupport
POSTGRES_PASSWORD=replace-with-a-long-random-password
REDIS_URL=redis://redis:6379
OPENAI_API_KEY=replace-with-your-api-key
JWT_SECRET=replace-with-a-32-byte-random-string
BOT_TOKEN=replace-with-your-bot-token
EOF
chmod 600 .envFile permissions matter here. Your API keys and webhook token should not be readable by other local users.
Create the Compose file.
cat > docker-compose.yml <<'EOF'
services:
postgres:
image: pgvector/pgvector:pg16
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7.4-alpine
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redisdata:/data
app:
image: node:22-alpine
restart: unless-stopped
working_dir: /app
env_file: .env
command: sh -c "npm install && npm run start"
ports:
- "127.0.0.1:${APP_PORT}:3000"
volumes:
- ./app:/app
depends_on:
- postgres
- redis
volumes:
pgdata:
redisdata:
EOFThis example assumes your application code lives in /opt/ai-support-bot/app. If you already have a bot project, copy it there before starting the stack.
Validate the Compose file before bringing anything up.
docker compose configSuccessful output means the YAML and environment references are valid.
Put a simple support bot app in place
If your bot already exists, adapt the same file structure. The goal here is to show a working private AI support bot on VPS from a clean start.
mkdir -p app
cat > app/package.json <<'EOF'
{
"name": "ai-support-bot",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.21.2",
"pg": "^8.13.1",
"redis": "^4.7.0"
}
}
EOF
cat > app/server.js <<'EOF'
import express from 'express';
import { Pool } from 'pg';
import { createClient } from 'redis';
const app = express();
app.use(express.json());
const pool = new Pool({
host: 'postgres',
port: 5432,
database: process.env.POSTGRES_DB,
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
});
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
app.get('/health', async (_req, res) => {
const db = await pool.query('SELECT 1 AS ok');
const cache = await redis.ping();
res.json({ ok: true, db: db.rows[0].ok === 1, cache });
});
app.post('/webhook', async (req, res) => {
const event = req.body?.event || 'unknown';
await redis.lPush('webhook-events', JSON.stringify({ event, at: new Date().toISOString() }));
res.json({ accepted: true });
});
app.listen(3000, '0.0.0.0', () => {
console.log('AI support bot listening on 3000');
});
EOFThat app gives you a health endpoint and a webhook endpoint. It is enough to prove the deployment path before you connect an actual LLM provider or CRM.
Start the stack and check the containers.
docker compose up -d
docker compose psYou should see three running services. If the app container exits, inspect logs before moving on.
docker compose logs --tail=100 appRoute traffic through Nginx and add SSL
Use Nginx as the public entry point, then keep the app bound to localhost. That reduces accidental exposure and makes rollback easier.
Create the reverse proxy configuration.
sudo tee /etc/nginx/conf.d/ai-support-bot.conf >/dev/null <<'EOF'
server {
listen 80;
server_name bot.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;
}
}
EOFReplace bot.example.com with your real domain. Check the syntax before reloading Nginx.
sudo nginx -t
sudo systemctl reload nginxNext, issue a TLS certificate with Certbot.
Ubuntu and Debian
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d bot.example.comAlmaLinux and Rocky Linux
sudo dnf install -y certbot python3-certbot-nginx
sudo certbot --nginx -d bot.example.comCertbot will rewrite the server block for HTTPS and set up renewals. Confirm the timer or cron job exists afterward.
sudo certbot renew --dry-runFor broader deployment options, compare this with OpenClaw, Coolify, Dokploy, or CapRover. If you want an open-source PaaS layer later, Hostperl’s OpenClaw hosting tutorial is a useful follow-up.
Test the bot from the server and from your browser
Now verify every layer in order: local port, Nginx, HTTPS, and the application endpoints.
On the VPS, confirm the app listens only on localhost.
ss -lntp | grep 3000
curl -s http://127.0.0.1:3000/health | jq .If you do not have jq, install it or read the raw JSON. You want ok: true, a working database check, and a Redis ping.
From your local computer, test the public site and webhook.
curl -I https://bot.example.com
curl -s -X POST https://bot.example.com/webhook -H 'Content-Type: application/json' -d '{"event":"test"}'The first command should return 200 or 301/302 redirecting to HTTPS. The second should return {"accepted":true}.
Check the Nginx and app logs if either request fails.
sudo tail -n 50 /var/log/nginx/error.log
docker compose logs --tail=50 appControl cost, latency, and exposure
A private AI support bot on VPS works best when the plan matches the workload. Smaller models, caching, and fewer retries usually save more than buying a larger server too early.
- Keep the bot on a right-sized Hostperl VPS for the first release, then scale only when CPU, memory, or queue depth justify it.
- Cache repeated retrieval results in Redis for FAQ-heavy chats and repeated sales queries.
- Store embeddings in pgvector if you want fewer moving parts and easier backups.
- Use environment variables for API keys and webhook tokens, never in source files.
- Limit bot permissions to the channels, inboxes, and tool calls it truly needs.
For data-sensitive deployments, the combination of a New Zealand or nearby APAC VPS, private networking choices, and local backups can make procurement and compliance conversations simpler. That is one reason many teams choose Hostperl VPS instead of a serverless patchwork.
Back up, restart, and prove persistence
Before you call the rollout finished, confirm that the stack survives a reboot and that your data is still there.
docker compose exec postgres pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"
docker compose restart
sudo rebootAfter the server comes back, reconnect and check the service list again.
ssh ADMIN_USER@203.0.113.10
cd /opt/ai-support-bot
docker compose ps
systemctl status nginx --no-pager
docker compose logs --tail=30 appYou should see Nginx active, Docker active, and the app container running again. If the bot uses scheduled jobs or workers, verify those containers separately.
Troubleshooting the most likely failures
These are the issues that usually show up in support tickets after the first launch.
- 502 Bad Gateway: run
docker compose logs --tail=100 app. If the app crashed, fix the Node error and start it again withdocker compose up -d. - Nginx syntax error: run
sudo nginx -t. The output usually points to the exact line in/etc/nginx/conf.d/ai-support-bot.conf. - Database connection refused: run
docker compose psanddocker compose logs --tail=100 postgres. If the volume is empty or the password changed, update.envand restart the stack. - Webhook does not arrive: run
curl -v https://bot.example.com/webhookand inspect any provider-side retry logs. Confirm your firewall allows 443 and the DNS record points to the right IP. - SSL renewal failure: run
sudo certbot renew --dry-run. If validation fails, confirm the domain resolves to this server before the renewal window.
Hostperl gives you the kind of VPS setup that fits private AI work: predictable access, straightforward support, and room to run your bot, database, and worker jobs on your terms. If you are planning a production rollout, start with Hostperl VPS hosting and keep your AI app behind your own reverse proxy.
If your team needs a more durable layout for customer-service automations or retrieval-heavy workflows, this is also a good place to compare your stack with private AI app hosting guidance before you go live.
FAQ
Should I run a private AI support bot on VPS or use serverless?
Use a VPS when you need stable latency, tighter data control, and easier inspection of logs, queues, and backups. Serverless is convenient, but private support workflows usually need more consistent behavior.
Is pgvector enough for a small RAG-backed bot?
Yes, for many customer-support and knowledge-base use cases. Start there before adding a separate vector database unless you have a clear scale or search requirement.
How do I keep secrets out of the repo?
Store them in a root-owned or user-owned .env file with chmod 600, or move to a secret manager later. Do not commit API keys, tokens, or database passwords.
What should I check after a migration?
Check the health endpoint, webhook delivery, database reads and writes, SSL renewal, and reboot persistence. Those five checks catch most launch problems early.
Can Hostperl help if I need a bigger plan later?
Yes. If your bot grows into a multi-tenant app, a heavier VPS or dedicated server is often easier to manage than stitching together multiple managed services.
For teams that want a practical, supportable setup in 2026, this private AI support bot on VPS pattern stays easy to explain, back up, and recover. When you are ready to expand, keep the same layout and move up to a larger managed VPS hosting plan instead of redesigning the whole stack.
