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

Host AI Workflows on VPS: Secure 2026 Setup

By Raman Kumar

Share:

Updated on Jul 29, 2026

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.10

Replace 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-release

You 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 gnupg

This updates package lists and installs the basic tools you will use later.

adduser aiadmin
usermod -aG sudo aiadmin

Set 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_keys

If 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 -v

You 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 aiadmin

Now 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_keys

Open a second terminal and confirm the new account works.

ssh aiadmin@203.0.113.10
sudo -v

After 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 nginx

Check the versions so you know the stack is ready.

docker --version
docker compose version
nginx -v

AlmaLinux or Rocky Linux

sudo dnf install -y docker docker-compose-plugin nginx certbot python3-certbot-nginx
sudo systemctl enable --now docker nginx

Verify the binaries.

docker --version
docker compose version
nginx -v

If 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
pwd

You should see /opt/aiflow. Now create the application files.

mkdir -p app

Open 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 --build

Then 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 ps

You 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;
    }
}
EOF

Test the configuration before reloading. This keeps a bad edit from breaking traffic.

sudo nginx -t

If the syntax is OK, reload Nginx.

sudo systemctl reload nginx

Open 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 status

AlmaLinux 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-all

If you use AlmaLinux or Rocky Linux with SELinux enforcing, Nginx usually needs permission to connect upstream.

sudo setsebool -P httpd_can_network_connect 1

Add 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.nz

Choose 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/.env

In 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 app

Look 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 3000 should show the app bound only to 127.0.0.1:3000.
  • Service status: systemctl status nginx docker should show both active.
  • Container health: docker compose ps should show all services up.
  • Reboot persistence: sudo reboot, then reconnect and run docker compose ps again.

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.