Host AI Workflows on VPS: Secure 2026 Setup

What you need before you host AI workflows on VPS
If you want to host AI workflows on VPS in 2026, start with a workload that matches the server you buy. A support bot, a sales qualifier, and a document retrieval app all behave differently, even if they call the same model API. Hostperl’s VPS hosting fits these cases better than serverless because you control the reverse proxy, environment variables, queues, logs, and backup cadence.
This tutorial walks through a practical private AI app: a Node.js webhook service that receives chat requests, stores conversation state, and calls a model API. You will install Docker, run the app with Docker Compose, place Nginx in front, add TLS, and verify the service from both the server and a client. If you want more context on deployment choices, the notes in AI bot hosting decisions for VPS, RAG, and private deployments and private AI hosting for bots, RAG, and Next.js apps are a useful companion read.
- Example app: Node.js 20 webhook service
- Example stack: Docker Compose, PostgreSQL, Redis, Nginx, Certbot
- Example domain: ai.example.nz
- Example server: 2 vCPU, 4 GB RAM for a small bot, 4 vCPU or more if you expect queue growth
First login, OS check, and safe admin setup
Open a terminal on your local computer and connect as root. Leave this session open until you confirm the new sudo user works.
ssh root@203.0.113.10Replace 203.0.113.10 with your VPS IP. If your provider gives you a non-root login, use that account name instead. Next, confirm the OS so you use the right package manager.
cat /etc/os-releaseYou should see either Ubuntu/Debian or AlmaLinux/Rocky Linux. The next steps differ.
Ubuntu or Debian
Run these commands on the VPS as root. They create a sudo user called aiadmin, add SSH access, and keep root login available until you test the new account.
apt update
apt install -y sudo curl git ufw ca-certificates gnupgThis updates package lists and installs the basic tools you will use later.
adduser aiadmin
usermod -aG sudo aiadminSet a strong password when prompted, then prepare SSH access.
mkdir -p /home/aiadmin/.ssh
chmod 700 /home/aiadmin/.ssh
cp /root/.ssh/authorized_keys /home/aiadmin/.ssh/authorized_keys
chown -R aiadmin:aiadmin /home/aiadmin/.ssh
chmod 600 /home/aiadmin/.ssh/authorized_keysIf you use a different public key, paste it into /home/aiadmin/.ssh/authorized_keys instead. Now open a second terminal on your local computer and test the new login.
ssh aiadmin@203.0.113.10
sudo -vYou should be prompted for the new user’s password, then for sudo access. Do not close the root session yet.
AlmaLinux or Rocky Linux
Run these commands on the VPS as root. They use the wheel group instead of sudo.
dnf install -y sudo curl git firewalld ca-certificates
useradd -m -s /bin/bash aiadmin
passwd aiadmin
usermod -aG wheel aiadminNow add SSH keys and permissions.
mkdir -p /home/aiadmin/.ssh
chmod 700 /home/aiadmin/.ssh
cp /root/.ssh/authorized_keys /home/aiadmin/.ssh/authorized_keys
chown -R aiadmin:aiadmin /home/aiadmin/.ssh
chmod 600 /home/aiadmin/.ssh/authorized_keysOpen a second terminal and confirm the new account works.
ssh aiadmin@203.0.113.10
sudo -vAfter the new login succeeds, you can disable root password login later. Keep the original root session open until the end of the setup.
Install Docker, Nginx, and the runtime pieces
Use Docker Compose so your app, database, and cache stay together. That makes restarts predictable and rollbacks easier for support teams.
Ubuntu or Debian
sudo apt install -y docker.io docker-compose-plugin nginx certbot python3-certbot-nginx
sudo systemctl enable --now docker nginxCheck the versions so you know the stack is ready.
docker --version
docker compose version
nginx -vAlmaLinux or Rocky Linux
sudo dnf install -y docker docker-compose-plugin nginx certbot python3-certbot-nginx
sudo systemctl enable --now docker nginxVerify the binaries.
docker --version
docker compose version
nginx -vIf you are running a heavier retrieval stack, the same pattern works for private RAG stacks, pgvector services, or queue workers that need steady memory. For teams that want a visual app layer, OpenClaw hosting for AI agents and RAG apps fits the same VPS model.
Build the AI workflow app
Create a working directory and an app user on the VPS as the non-root sudo user.
sudo mkdir -p /opt/aiflow
sudo chown -R aiadmin:aiadmin /opt/aiflow
cd /opt/aiflow
pwdYou should see /opt/aiflow. Now create the application files.
mkdir -p appOpen app/package.json and paste this content.
{
"name": "aiflow",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.21.2",
"pg": "^8.13.1",
"redis": "^4.7.0"
}
}Create app/server.js with this minimal webhook service. It stores requests, caches recent prompts, and returns a short reply placeholder you can replace with your model call later.
import express from 'express';
import pg from 'pg';
import { createClient } from 'redis';
const app = express();
app.use(express.json({ limit: '1mb' }));
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
app.get('/healthz', async (_req, res) => {
const db = await pool.query('SELECT 1 AS ok');
res.json({ ok: true, db: db.rows[0].ok === 1 });
});
app.post('/webhook/chat', async (req, res) => {
const { session_id, message } = req.body;
if (!session_id || !message) {
return res.status(400).json({ error: 'session_id and message are required' });
}
await pool.query(
'INSERT INTO messages(session_id, message, created_at) VALUES ($1, $2, NOW())',
[session_id, message]
);
await redis.set(`last:${session_id}`, message, { EX: 3600 });
res.json({
reply: 'Thanks. Your request was received and queued for the AI worker.',
cached_last_message: await redis.get(`last:${session_id}`)
});
});
app.listen(process.env.PORT || 3000, '0.0.0.0');Create the Docker Compose file next.
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: aiflow
POSTGRES_USER: aiflow
POSTGRES_PASSWORD: change-this-password
volumes:
- dbdata:/var/lib/postgresql/data
restart: unless-stopped
redis:
image: redis:7-alpine
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redisdata:/data
restart: unless-stopped
app:
build: ./app
environment:
PORT: 3000
DATABASE_URL: postgres://aiflow:change-this-password@db:5432/aiflow
REDIS_URL: redis://redis:6379
ports:
- "127.0.0.1:3000:3000"
depends_on:
- db
- redis
restart: unless-stopped
volumes:
dbdata:
redisdata:Add a simple Dockerfile in app/Dockerfile.
FROM node:20-alpine
WORKDIR /app
COPY package.json ./
RUN npm install --omit=dev
COPY server.js ./
EXPOSE 3000
CMD ["npm", "start"]Now create the database table. First start the stack.
docker compose up -d --buildThen create the table from inside PostgreSQL.
docker compose exec db psql -U aiflow -d aiflow -c "CREATE TABLE IF NOT EXISTS messages (id SERIAL PRIMARY KEY, session_id TEXT NOT NULL, message TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW());"Check that the containers are running.
docker compose psYou should see app, db, and redis in the Up state.
Put Nginx in front of the app
Create a site file for your domain. Replace ai.example.nz with your real hostname.
sudo tee /etc/nginx/conf.d/aiflow.conf >/dev/null <<'EOF'
server {
listen 80;
server_name ai.example.nz;
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 configuration before reloading. This keeps a bad edit from breaking traffic.
sudo nginx -tIf the syntax is OK, reload Nginx.
sudo systemctl reload nginxOpen the firewall safely.
Ubuntu or Debian
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
sudo ufw statusAlmaLinux or 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-allIf you use AlmaLinux or Rocky Linux with SELinux enforcing, Nginx usually needs permission to connect upstream.
sudo setsebool -P httpd_can_network_connect 1Add TLS and environment variables
Before you expose the app publicly, point DNS for ai.example.nz to the VPS. Then request a certificate.
sudo certbot --nginx -d ai.example.nzChoose the redirect option when prompted. Certbot updates the Nginx file and reloads it after validation. For DNS and domain planning around AI apps, the notes in Hostperl SSL, DNS, and email setup tutorial help when you are moving multiple services at once.
Now create a secrets file for the app. Keep it out of git.
sudo tee /opt/aiflow/.env >/dev/null <<'EOF'
PORT=3000
DATABASE_URL=postgres://aiflow:change-this-password@db:5432/aiflow
REDIS_URL=redis://redis:6379
MODEL_API_KEY=replace-with-your-key
EOF
sudo chmod 600 /opt/aiflow/.env
sudo chown aiadmin:aiadmin /opt/aiflow/.envIn a real deployment, add your model provider key here, then reference it from your app code. That keeps secrets out of logs and shell history.
Restart, test, and confirm the app works
Recreate the app container so it loads the environment file, then inspect logs.
cd /opt/aiflow
docker compose down
docker compose up -d --build
docker compose logs --tail=50 appLook for a clean startup and no connection errors to PostgreSQL or Redis. Check local health from the VPS.
curl -s http://127.0.0.1:3000/healthz | jq .If jq is missing, install it with your package manager and rerun the command. Then test from your laptop or desktop against the public domain.
curl -s https://ai.example.nz/healthz | jq .
curl -s -X POST https://ai.example.nz/webhook/chat \
-H 'Content-Type: application/json' \
-d '{"session_id":"demo-001","message":"What is my order status?"}' | jq .You should get a JSON reply and see the last message cached. That confirms the reverse proxy, TLS, app container, database, and Redis are all working.
Operational checks for private AI deployments
Run these checks before handing the service to a client, support team, or internal staff. They help you catch the failures that usually show up first.
- Port binding:
ss -tulpn | grep 3000should show the app bound only to127.0.0.1:3000. - Service status:
systemctl status nginx dockershould show both active. - Container health:
docker compose psshould show all services up. - Reboot persistence:
sudo reboot, then reconnect and rundocker compose psagain.
For more advanced bot planning, especially around support automation and retrieval, running a private AI support bot on VPS covers the next layer. If your workload grows into document search, RAG app hosting with pgvector and Docker Compose is the natural follow-up.
Troubleshooting the issues you will actually see
502 Bad Gateway: run sudo tail -n 50 /var/log/nginx/error.log. If you see connect() failed, the app is not listening or Docker Compose is down. Fix it with docker compose up -d --build.
Database connection errors: run docker compose logs --tail=50 db. If PostgreSQL is restarting, the password in .env and Compose must match. Update both, then run docker compose up -d again.
TLS failure: run sudo certbot certificates. If your domain is missing, DNS has not propagated or points to the wrong IP. Correct the A record, wait, then rerun Certbot.
High token spend: check your app logs and provider dashboard together. Cache repeated retrieval results in Redis, trim system prompts, and route simple intents to a smaller model before you escalate to a larger one.
If you are ready to host AI workflows on VPS with tighter control over data, logs, and costs, Hostperl can help you choose the right server size and deployment path. For private bots, RAG stacks, and secure app hosting, start with Hostperl VPS hosting and scale only when your queue depth or latency shows you need more.
For teams building customer-service automations or internal AI tools, Hostperl gives you room to handle migrations, SSL, backups, and rollback planning without handing your workflow to a serverless platform you cannot inspect.
FAQ
Can I host a support bot and a RAG app on the same VPS?
Yes, if you size the CPU and RAM properly. Keep the bot API, vector store, and worker queue separated in Docker Compose so restarts stay predictable.
Is VPS hosting better than Vercel or Render for private AI apps?
If you need data residency, local logs, fixed upstream ports, or a persistent Redis and PostgreSQL stack, a VPS is usually easier to control.
What size VPS should I start with?
For a light support bot, start at 2 vCPU and 4 GB RAM. For retrieval, embeddings, or background jobs, 4 vCPU and 8 GB RAM is a safer first step.
How do I keep AI secrets out of the codebase?
Store them in a root-owned or app-owned .env file with chmod 600, or move them to a managed secret store if your deployment stack supports it.
What should I back up first?
Back up the database volume, the Compose file, the Nginx config, and the .env file. Those four items are enough to rebuild most small AI workflows quickly.
