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.10203.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.10On the VPS as root: confirm the OS before you install anything.
cat /etc/os-releaseYou 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 deployEnter a strong password when prompted. This creates the account safely before you move SSH access over.
usermod -aG sudo deployThis 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/.sshThese 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.10Then test sudo:
sudo -vIf 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 unzipUbuntu 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
hostnamectlThat 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-pagerYou 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;
EOFIf 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
pwdYou 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 wheelUsing 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/.envThe .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
EOFNow 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-pagerIf 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 verboseThis 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/defaultTest the config before reload. That catches syntax mistakes before they affect live traffic.
sudo nginx -t
sudo systemctl reload nginx
systemctl status nginx --no-pagerIf 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/healthYou 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/healthReplace 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.comChoose 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-runThe 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_configSet these values in the file:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yesThen test the config and reload SSH:
sudo sshd -t
sudo systemctl reload sshKeep 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-pagerFor 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-pagerIf 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/.envIf 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.serviceshowsactive (running)systemctl status nginxshowsactive (running)sudo ufw status verboseallows SSH and Nginxcurl -s http://127.0.0.1:8000/healthreturns database OKcurl -I https://example.comreturns a valid HTTPS response after TLS setupsudo systemctl is-enabled ragapp.servicereturnsenabled
On the VPS as the non-root sudo user:
systemctl is-enabled ragapp.service
systemctl is-enabled nginx
sudo rebootAfter 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.
