Skip to main content

VPS Deployment Guide

This document covers the exact steps used to deploy TradeEntry on the production VPS, including all configuration, fixes, and troubleshooting discovered along the way.

Server Details

ItemValue
VPS IP103.178.166.188
OSUbuntu 22.04
Userkss
Main sitehttps://tradeentry.in
Docs sitehttps://docs.tradeentry.in
RegistrarGoDaddy
Web servernginx
SSLLet's Encrypt / Certbot
DatabasePostgreSQL 16 via Docker

1. DNS Configuration (GoDaddy)

Login to GoDaddy DNS Manager and create these records:

TypeNameValueTTL
A@103.178.166.188600
Adocs103.178.166.188600
CNAMEwwwtradeentry.in1 Hour

Remove any existing GoDaddy Website Builder entries, domain forwarding, or parking/redirect rules.

Wait 5–30 minutes for DNS to propagate, then verify from Windows CMD:

nslookup tradeentry.in
ipconfig /flushdns

2. Connect to VPS

ssh kss@103.178.166.188

3. Install nginx + required system packages

sudo apt update
sudo apt install nginx -y
sudo systemctl enable nginx
sudo systemctl start nginx
sudo systemctl status nginx

Expected: active (running)

Also install the system-level tools the backend shells out to — these are apt packages, NOT Python packages, so neither pull.sh nor uv run app.py will ever install them for you:

sudo apt install unrar postgresql-client -y
PackageUsed byIf missing
unrarEODIEOD pipeline (nseieod/eodieod/rario.py) — extracts the 4 daily vendor .rar filesPipeline raises "No RAR extraction tool found" — or, via the web page's background task, appears to silently hang at 0% (2026-07-14 incident)
postgresql-clientpsql/pg_dump on the VPS shell (DB swaps, backups, sync scripts)Manual DB operations in the Sync Process doc fail

4. Firewall Setup

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow OpenSSH
sudo ufw enable
sudo ufw status
note

Ports 8000, 5173, 3000 are kept internal only — all external traffic goes through nginx on 80/443.


5. Install Node.js (via nvm — no sudo needed)

The system Node.js may be too old (v12). Use nvm:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc
nvm install 20
node --version # should show v20.x.x
npm --version

6. Install Python (via uv)

curl -LsSf https://astral.sh/uv/install.sh | sh
source ~/.bashrc
uv python install 3.11
uv --version

7. Clone the Repository

Generate a deploy key for the private GitHub repo:

ssh-keygen -t ed25519 -C "tradeentry" -f ~/.ssh/tradeentry_deploy
cat ~/.ssh/tradeentry_deploy.pub

Add the public key to GitHub → Repository → Settings → Deploy Keys.

Then clone:

eval "$(ssh-agent -s)"
ssh-add ~/.ssh/tradeentry_deploy
git clone git@github.com:srchains/tradeentry.git ~/tradeentry
cd ~/tradeentry

Configure git to use the deploy key going forward:

# ~/.ssh/config
Host github.com
IdentityFile ~/.ssh/tradeentry_deploy
IdentitiesOnly yes

8. Configure backend .env

nano ~/tradeentry/backend/.env
APP_ENV=VPS

# VPS DB (Docker PostgreSQL on 127.0.0.1)
VPS_DB_HOST=127.0.0.1
VPS_DB_NAME=tedb
VPS_DB_USER=postgres
VPS_DB_PASS=tedb

# Paths (created automatically on startup)
VPS_CSV_PATH=/home/kss/tradeentry/csvs
VPS_AMI_PATH=/home/kss/tradeentry/csvs/AMI

# Common
DB_PORT=5432
POSTGRES_PASSWORD=tedb
DOCS_URL=https://docs.tradeentry.in
Important — use 127.0.0.1 not localhost for DB_HOST

On Linux, localhost resolves to a Unix socket. Docker PostgreSQL only exposes TCP on 127.0.0.1. Using localhost will cause connection refused errors even when Docker is running fine.


9. PostgreSQL via Docker

The database runs as a Docker container. Start and configure it:

# Pull and start PostgreSQL 16
docker run -d \
--name postgres-db \
--restart=always \
-e POSTGRES_PASSWORD=tedb \
-e POSTGRES_DB=tedb \
-p 5432:5432 \
postgres:16

# Verify it's running
docker ps

Make sure it survives reboots:

docker update --restart=always postgres-db

Test the connection:

PGPASSWORD=tedb psql -h 127.0.0.1 -U postgres -d tedb -c "\dt"

Check row counts:

PGPASSWORD=tedb psql -h 127.0.0.1 -U postgres -d tedb \
-c "SELECT COUNT(*) FROM scr_nseeq_eod;"

10. nginx Configuration

Main site — tradeentry.in

sudo nano /etc/nginx/sites-available/tradeentry.in
server {
server_name tradeentry.in www.tradeentry.in;

# Backend API — proxy to FastAPI on port 8000
location /api/ {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
# EODIEOD .rar uploads can be 100s of MB (Option file ~170MB+) --
# nginx's default client_max_body_size (1MB) silently 413s these
# before the request ever reaches FastAPI's own 300MB check. Set
# above that backend limit so nginx is never the tighter one.
client_max_body_size 350M;
}

# WebSocket connections
location /ws/ {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}

# Frontend — Vite preview on port 5173
location / {
proxy_pass http://127.0.0.1:5173;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}

listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/tradeentry.in/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/tradeentry.in/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}

server {
if ($host = www.tradeentry.in) { return 301 https://$host$request_uri; }
if ($host = tradeentry.in) { return 301 https://$host$request_uri; }
listen 80;
server_name tradeentry.in www.tradeentry.in;
return 404;
}
/api/ block MUST come before location /

nginx uses longest-prefix matching. If location / comes first in the file it still works, but put API blocks above / as a convention to make the intent clear. The critical fix here: without these blocks, the browser calls https://tradeentry.in:8000/api directly — which is HTTP-only — and the browser blocks it as a mixed-content error.

Docs site — docs.tradeentry.in

sudo nano /etc/nginx/sites-available/docs.tradeentry.in
server {
server_name docs.tradeentry.in;

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-Proto $scheme;
}

listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/tradeentry.in/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/tradeentry.in/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}

server {
if ($host = docs.tradeentry.in) { return 301 https://$host$request_uri; }
listen 80;
server_name docs.tradeentry.in;
return 404;
}

Enable sites

sudo ln -s /etc/nginx/sites-available/tradeentry.in /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/docs.tradeentry.in /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx

11. SSL / HTTPS with Certbot

sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d tradeentry.in -d www.tradeentry.in -d docs.tradeentry.in

Choose redirect HTTP → HTTPS when prompted.

Certbot automatically edits the nginx config to add SSL certificate paths.

Verify auto-renewal:

sudo certbot renew --dry-run

12. First-time Build and Start

cd ~/tradeentry
APP_ENV=VPS uv run app.py

app.py will:

  1. Check system dependencies
  2. Create Python venv and install requirements
  3. Run npm install for frontend and docs
  4. Build frontend (npm run build) and docs (npm run build)
  5. Start all three services (FastAPI, Vite preview, Docusaurus serve)

This takes 5–10 minutes on first run. After it completes, all services are live.


13. systemd Service (Auto-start on Reboot)

Create the startup wrapper

nano ~/tradeentry/start.sh
#!/bin/bash
# Load nvm so node/npm are on PATH
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

# uv installs to ~/.local/bin
export PATH="$HOME/.local/bin:$PATH"

export APP_ENV=VPS

cd /home/kss/tradeentry
exec uv run app.py --no-build
chmod +x ~/tradeentry/start.sh

Create the service file

sudo nano /etc/systemd/system/tradeentry.service
[Unit]
Description=TradeEntry Trading Platform
After=network.target docker.service
Wants=docker.service

[Service]
Type=simple
User=kss
WorkingDirectory=/home/kss/tradeentry
ExecStart=/home/kss/tradeentry/start.sh
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Enable and start

sudo systemctl daemon-reload
sudo systemctl enable tradeentry
sudo systemctl start tradeentry
sudo systemctl status tradeentry

Service management commands

sudo systemctl status tradeentry # check status
sudo journalctl -u tradeentry -f # live logs
sudo journalctl -u tradeentry -n 50 # last 50 lines
sudo systemctl restart tradeentry # restart
sudo systemctl stop tradeentry # stop

14. Deploying Code Updates

cd ~/tradeentry
git pull

# If only backend/config changes (no frontend code changed):
sudo systemctl restart tradeentry

# If frontend or docs changed — rebuild then restart:
sudo systemctl stop tradeentry
APP_ENV=VPS uv run app.py # builds + starts interactively
# Once confirmed working, Ctrl+C then:
sudo systemctl start tradeentry

15. Running Bhav Download Manually

cd ~/tradeentry/backend
APP_ENV=VPS ../backend/venv/bin/python -m nseeod.downloadbhav

Or via the web UI at https://tradeentry.in/bhav-downloader.


Troubleshooting

psql fails with "no such file or directory"

Symptom:

psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: No such file or directory

Cause: psql tries a Unix socket by default. The PostgreSQL database is running inside Docker, which only exposes TCP.

Fix: Always connect via TCP with -h 127.0.0.1:

PGPASSWORD=tedb psql -h 127.0.0.1 -U postgres -d tedb -c "SELECT 1;"

pg_lsclusters shows down but port 5432 is occupied

Symptom: Native pg_ctlcluster 14 main start fails with "Address already in use".

Cause: Docker is running PostgreSQL on port 5432. The native PostgreSQL cluster cannot also bind that port.

Fix: Use Docker PostgreSQL — it IS the database. Do not try to start the native cluster. Check Docker:

docker ps | grep postgres

If the Docker container is missing:

docker start postgres-db

LAST SYNC shows "---" / "Calculating..."

Cause 1 — CORS (pre-nginx): Backend returns correct data but CORS blocks the browser. Fixed by setting allow_origins=["*"] in backend/app/main.py when APP_ENV=VPS.

Cause 2 — Mixed content (post-nginx): The frontend is served over HTTPS but tries to call https://tradeentry.in:8000/api directly — browsers block this as a mixed-content error.

Visible in DevTools Console:

Failed to load resource: net::ERR_SSL_PROTOCOL_ERROR :8000/api/bhav-downloader/last-update

Fix: The frontend now uses relative /api URLs. nginx proxies /api/ → port 8000. The nginx /api/ location block must be present in the tradeentry.in config.

Cause 3 — PostgreSQL down: Docker container stopped or VPS rebooted without --restart=always.

docker ps # check if postgres-db is up
docker start postgres-db # start if stopped
docker update --restart=always postgres-db # prevent future occurrences

File upload fails with "413 Request Entity Too Large" / status code 413

Symptom: Uploading a large file (e.g. an EODIEOD .rar on the EODIEOD Upload page) fails with a generic Request failed with status code 413 in the browser console — no specific error message, just the bare axios error.

Cause: nginx's default client_max_body_size is only 1MB. It rejects the request itself, before it ever reaches FastAPI — which is exactly why the error is generic rather than one of the backend's own specific messages (e.g. the 300MB-per-file check in eodieod_upload.py would say "... file exceeds 300MB limit" if the request had actually reached it).

How to tell which layer rejected it: check the failed request's Response body in DevTools → Network. Plain/blank body (nginx's own error page) means nginx; a JSON body with a detail field means the backend's own check fired instead — in that case, raise _MAX_UPLOAD_BYTES in backend/app/api/eodieod_upload.py, not nginx.

Fix (nginx layer): add client_max_body_size to the /api/ location block in /etc/nginx/sites-available/tradeentry.in (see the config in section 10 above — already includes this):

sudo nano /etc/nginx/sites-available/tradeentry.in
# add: client_max_body_size 350M; inside location /api/ { ... }
sudo nginx -t && sudo systemctl reload nginx

systemd service fails with status=127

Symptom: journalctl -u tradeentry shows start.sh: uv: command not found.

Cause: systemd runs with a minimal PATH that does not include ~/.local/bin where uv is installed.

Fix: Add to start.sh:

export PATH="$HOME/.local/bin:$PATH"

Also add nvm loader for node/npm:

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

Vite preview blocks tradeentry.in hostname

Symptom: nginx proxies correctly but Vite returns 403 Forbidden or host rejection error.

Fix: Add allowedHosts to frontend/vite.config.js:

preview: {
port: 5173,
host: true,
allowedHosts: ['tradeentry.in', 'www.tradeentry.in', 'localhost'],
}

Symptom: "View Documentation" link becomes https://tradeentry.in:3000 which fails because port 3000 is HTTP only.

Fix: Two parts:

  1. Home.jsx uses http:// explicitly for docs link (not window.location.protocol)
  2. docs.tradeentry.in nginx proxy routes to port 3000 over HTTPS

npm not found when running app.py on Windows

Cause: On Windows, npm is npm.cmd — not in PATH as plain npm.

Fix: Already handled in app.py with shutil.which("npm").


Old Node.js version (v12) on Ubuntu

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc
nvm install 20

Quick Reference

TaskCommand
Check all servicessudo systemctl status tradeentry nginx docker
View app logssudo journalctl -u tradeentry -f
Restart appsudo systemctl restart tradeentry
Check DB rowsPGPASSWORD=tedb psql -h 127.0.0.1 -U postgres -d tedb -c "SELECT COUNT(*) FROM scr_nseeq_eod;"
Reload nginxsudo nginx -t && sudo systemctl reload nginx
Kill ports manuallyfuser -k 8000/tcp 5173/tcp 3000/tcp
Git pull + restartcd ~/tradeentry && git pull && sudo systemctl restart tradeentry
Check Dockerdocker ps
Start Docker DBdocker start postgres-db
Live rebuildAPP_ENV=VPS uv run app.py
Skip rebuildAPP_ENV=VPS uv run app.py --no-build

Last updated: 2026-05-25