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

RAG Application Hosting on Ubuntu Server with PostgreSQL

By Raman Kumar

Share:

Updated on Sep 24, 2026

RAG Application Hosting on Ubuntu Server with PostgreSQL

What you will build

This tutorial shows you how to deploy a small but production-minded RAG application hosting setup on Ubuntu Server 24.04 with PostgreSQL handling documents and vector search. You will create a non-root admin account, install the runtime, lock down the app directory, wire in systemd, put Nginx in front, and verify each layer with real checks.

The aim is a working service, not a throwaway demo. By the end, your app will start on boot, listen only where it should, and recover cleanly if a code change breaks it. If you are planning a private deployment on a Hostperl VPS, this is the sort of setup that support teams can actually help you maintain after launch.

For broader context on AI workloads and hosting decisions, see Private AI API security on VPS and private RAG stack hosting for smaller teams.

Prerequisites and design choices

Supported platform: Ubuntu Server 24.04 LTS. This procedure uses apt, systemd, UFW, AppArmor, and netplan. It stays Ubuntu-specific because the goal here is a clean, supportable deployment on the least-covered OS track for this run.

Architecture: Nginx terminates HTTP, your app listens on 127.0.0.1:8000, and PostgreSQL stores metadata plus vector data. That keeps the app off the public interface and gives you one place to manage TLS later.

Assumptions: your domain will point to example.com, your server hostname will be server.example.com, and your non-root admin account will be deploy. Replace those sample values with your real ones as you go.

1) Connect and confirm the operating system

On your local computer: connect as root first, then keep that session open until the new admin account works.

ssh root@203.0.113.10

203.0.113.10 is a reserved documentation address. Replace it with the real public IP assigned to your server.

If your provider gave you a default non-root account instead, use it from your local computer:

ssh deploy@203.0.113.10

On the VPS as root: confirm the OS before you install anything.

cat /etc/os-release

You should see Ubuntu 24.04 details. If the server is not Ubuntu, stop here and use a Linux branch that matches the platform.

2) Create the deploy user and SSH key login

On the VPS as root: create the non-root administrator, grant sudo, and prepare SSH key access. Do not disable root login yet.

adduser deploy

Enter a strong password when prompted. This creates the account safely before you move SSH access over.

usermod -aG sudo deploy

This adds deploy to the sudo group on Ubuntu.

Now create the SSH directory and authorize a key. Replace the example key text with your own public key from your local machine.

mkdir -p /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
cat > /home/deploy/.ssh/authorized_keys <<'EOF'
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleKeyReplaceThisWithYourOwnKey deploy@laptop
EOF
chmod 600 /home/deploy/.ssh/authorized_keys
chown -R deploy:deploy /home/deploy/.ssh

These permissions matter. OpenSSH will ignore a key file that is too permissive.

On your local computer: open a second terminal and test the new login before changing root access.

ssh deploy@203.0.113.10

Then test sudo:

sudo -v

If that works, leave the original root session open and continue from the new one.

3) Update the server and set basic host identity

On the VPS as the non-root sudo user: update the package index and install the first round of maintenance packages.

sudo apt update
sudo apt -y upgrade
sudo apt -y install ca-certificates curl git ufw unzip

Ubuntu Server 24.04 should apply security updates cleanly. If the kernel updates, reboot after the setup is complete and before production launch.

Set a clear hostname so logs and prompts match the server you are managing.

sudo hostnamectl set-hostname server.example.com
hostnamectl

That should show server.example.com. If you use a different hostname, update your DNS later to match it.

4) Install PostgreSQL for document and vector storage

On the VPS as the non-root sudo user: install PostgreSQL and confirm the service is active.

sudo apt -y install postgresql postgresql-contrib
systemctl status postgresql --no-pager

You want to see active (running). On Ubuntu 24.04, the package manager will install a supported PostgreSQL release from the Ubuntu repositories.

Create a database and an application role. Use a strong password and store it somewhere safe.

sudo -u postgres psql <<'EOF'
CREATE USER ragapp WITH PASSWORD 'ChangeThisToAStrongPassword';
CREATE DATABASE ragdb OWNER ragapp;
\c ragdb
CREATE EXTENSION IF NOT EXISTS vector;
EOF

If your PostgreSQL package set does not yet include pgvector, you will need to install the matching extension package for your repository or use your application's built-in vector store. Do not skip the database ownership step.

Confirm the database exists and the extension loaded:

sudo -u postgres psql -d ragdb -c "\dx"
sudo -u postgres psql -d ragdb -c "\du"

The extension list should include vector if pgvector is available in your package set.

5) Create the application directory and virtual environment

On the VPS as the non-root sudo user: keep application files outside your home directory so deployments stay predictable.

sudo mkdir -p /opt/myapp
sudo chown -R deploy:deploy /opt/myapp
cd /opt/myapp
pwd

You should now be inside /opt/myapp.

Install Python tooling and create a virtual environment for the app.

sudo apt -y install python3 python3-venv python3-pip build-essential libpq-dev
python3 -m venv .venv
. .venv/bin/activate
python -V
pip install --upgrade pip setuptools wheel

Using a virtual environment keeps the app isolated from system Python packages, which makes rollback easier.

6) Add the RAG application code and environment file

On the VPS as the non-root sudo user: create a minimal FastAPI app that checks PostgreSQL and responds through Nginx. Replace the sample token with a real value.

cat > /opt/myapp/app.py <<'EOF'
import os
from fastapi import FastAPI
import psycopg

app = FastAPI()
DATABASE_URL = os.environ["DATABASE_URL"]

@app.get("/")
def home():
    return {"status": "ok", "service": "rag-app"}

@app.get("/health")
def health():
    with psycopg.connect(DATABASE_URL) as conn:
        with conn.cursor() as cur:
            cur.execute("SELECT 1")
            cur.fetchone()
    return {"database": "ok"}
EOF

cat > /opt/myapp/requirements.txt <<'EOF'
fastapi==0.115.8
uvicorn[standard]==0.34.0
psycopg[binary]==3.2.4
EOF

cat > /opt/myapp/.env <<'EOF'
DATABASE_URL=postgresql://ragapp:ChangeThisToAStrongPassword@127.0.0.1:5432/ragdb
EOF

chmod 600 /opt/myapp/.env

The .env file contains credentials, so keep it readable only by the application owner.

Install the Python dependencies and confirm the app imports cleanly.

cd /opt/myapp
. .venv/bin/activate
pip install -r requirements.txt
python -c "import app; print('import ok')"

You should see import ok. If import fails, fix that before you create a service.

7) Create the systemd service

On the VPS as the non-root sudo user: create a dedicated unit so the app starts on boot and restarts after a crash.

sudo tee /etc/systemd/system/ragapp.service > /dev/null <<'EOF'
[Unit]
Description=RAG application service
After=network.target postgresql.service

[Service]
User=deploy
Group=deploy
WorkingDirectory=/opt/myapp
EnvironmentFile=/opt/myapp/.env
ExecStart=/opt/myapp/.venv/bin/uvicorn app:app --host 127.0.0.1 --port 8000
Restart=on-failure
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/myapp

[Install]
WantedBy=multi-user.target
EOF

Now test the unit file, reload systemd, and start the service.

sudo systemd-analyze verify /etc/systemd/system/ragapp.service
sudo systemctl daemon-reload
sudo systemctl enable --now ragapp.service
systemctl status ragapp.service --no-pager

If the status is active (running), the app is live on localhost port 8000.

8) Open the firewall safely

On the VPS as the non-root sudo user: allow SSH before you turn on the firewall, then permit Nginx.

sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw --force enable
sudo ufw status verbose

This sequence avoids locking yourself out. If SSH is already allowed, UFW will keep it open.

9) Install and configure Nginx as the reverse proxy

On the VPS as the non-root sudo user: install Nginx and create a server block that sends traffic to your app on 127.0.0.1:8000.

sudo apt -y install nginx
sudo tee /etc/nginx/sites-available/ragapp > /dev/null <<'EOF'
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    location / {
        proxy_pass http://127.0.0.1:8000;
        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 ln -s /etc/nginx/sites-available/ragapp /etc/nginx/sites-enabled/ragapp
sudo rm -f /etc/nginx/sites-enabled/default

Test the config before reload. That catches syntax mistakes before they affect live traffic.

sudo nginx -t
sudo systemctl reload nginx
systemctl status nginx --no-pager

If you use a different domain, replace example.com and www.example.com in the config before you run the syntax test.

10) Validate the app locally and from a client

On the VPS as the non-root sudo user: check the backend directly on loopback first.

curl -s http://127.0.0.1:8000/
curl -s http://127.0.0.1:8000/health

You should get JSON responses and no PostgreSQL error. If the health endpoint fails, check the database password in .env and the PostgreSQL status.

On your local computer: test the public endpoint after DNS points to the server.

curl -i http://example.com/
curl -i http://example.com/health

Replace example.com with your real domain. You should see a 200 response and the JSON payload from the app.

11) Add TLS with Let's Encrypt

On the VPS as the non-root sudo user: install Certbot for Nginx and request a certificate after DNS is correct and the port 80 site works.

sudo apt -y install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com

Choose the redirect option when prompted so HTTP moves to HTTPS. Certbot will update Nginx and reload it for you.

Confirm renewal is scheduled:

systemctl list-timers | grep certbot
sudo certbot renew --dry-run

The dry run should complete without errors. If it fails, check DNS, firewall rules, and whether port 80 is reachable from the public internet.

12) Lock down root access after verification

Only do this after the deploy login, sudo, firewall, app service, and HTTPS checks have all passed.

On the VPS as root, from your original session: disable password authentication if your SSH keys are working everywhere you need them.

sudoedit /etc/ssh/sshd_config

Set these values in the file:

PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes

Then test the config and reload SSH:

sudo sshd -t
sudo systemctl reload ssh

Keep the root session open until you open a fresh SSH session and confirm the new login still works. If you misconfigure SSH here, the syntax test usually catches it before lockout.

Rollback and recovery

If the app stops after a deploy, the safest rollback is usually to restore the previous code directory and restart the service.

sudo systemctl stop ragapp.service
cd /opt/myapp
ls -la
sudo systemctl start ragapp.service
sudo journalctl -u ragapp.service -n 50 --no-pager

For a failed Nginx change, restore the backup or remove the new site file, then run nginx -t before reloading.

If PostgreSQL becomes unavailable, confirm the service and socket state:

systemctl status postgresql --no-pager
sudo -u postgres psql -d ragdb -c "SELECT 1;"

That tells you whether the problem is the database engine, credentials, or the application code.

Troubleshooting the most likely failures

1. Nginx shows 502 Bad Gateway. Run:

sudo systemctl status ragapp.service --no-pager
sudo journalctl -u ragapp.service -n 100 --no-pager

If the app is crashing, fix the Python traceback, then restart the service with sudo systemctl restart ragapp.service.

2. The health endpoint fails with a database error. Check:

sudo -u postgres psql -d ragdb -c "SELECT 1;"
sudo cat /opt/myapp/.env

If the password is wrong, update .env, keep permissions at 600, and restart the app service.

3. Certbot cannot issue a certificate. Check DNS and inbound access:

sudo ufw status verbose
curl -I http://example.com

If HTTP does not answer publicly, fix the DNS record or firewall before retrying Certbot.

Final verification checklist

Before you call the deployment complete, confirm each layer:

  • systemctl status ragapp.service shows active (running)
  • systemctl status nginx shows active (running)
  • sudo ufw status verbose allows SSH and Nginx
  • curl -s http://127.0.0.1:8000/health returns database OK
  • curl -I https://example.com returns a valid HTTPS response after TLS setup
  • sudo systemctl is-enabled ragapp.service returns enabled

On the VPS as the non-root sudo user:

systemctl is-enabled ragapp.service
systemctl is-enabled nginx
sudo reboot

After reboot, reconnect and confirm the app still starts automatically. That is the real persistence test.

If you want to run a private RAG service without wasting time on unstable hosting, Hostperl can provide the VPS capacity and support workflow this kind of deployment needs. For teams that expect growth, a Hostperl VPS is a practical starting point, and larger deployments can move to dedicated server hosting when memory, storage, or isolation requirements rise.

Our support model fits launch work, post-cutover fixes, and the small operational issues that always appear after day one.

FAQ

Can I run this RAG application without Nginx?

Yes, but Nginx gives you cleaner TLS handling, better request logging, and a simpler path to rate limiting later.

Is PostgreSQL enough for vector search?

For smaller and mid-sized RAG workloads, yes. If your corpus grows quickly, you may later separate metadata, vectors, and search into different services.

Should I keep root SSH enabled?

No. Keep it enabled only until the deploy user is verified, then disable root login and password authentication if your key-based access is working.

What should I monitor first?

Watch service uptime, PostgreSQL connectivity, disk usage in /var/lib/postgresql, and the Nginx access and error logs. Those catch most early failures.