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
| Item | Value |
|---|---|
| VPS IP | 103.178.166.188 |
| OS | Ubuntu 22.04 |
| User | kss |
| Main site | https://tradeentry.in |
| Docs site | https://docs.tradeentry.in |
| Registrar | GoDaddy |
| Web server | nginx |
| SSL | Let's Encrypt / Certbot |
| Database | PostgreSQL 16 via Docker |
1. DNS Configuration (GoDaddy)
Login to GoDaddy DNS Manager and create these records:
| Type | Name | Value | TTL |
|---|---|---|---|
| A | @ | 103.178.166.188 | 600 |
| A | docs | 103.178.166.188 | 600 |
| CNAME | www | tradeentry.in | 1 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
| Package | Used by | If missing |
|---|---|---|
unrar | EODIEOD pipeline (nseieod/eodieod/rario.py) — extracts the 4 daily vendor .rar files | Pipeline 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-client | psql/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
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
127.0.0.1 not localhost for DB_HOSTOn 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:
- Check system dependencies
- Create Python venv and install requirements
- Run
npm installfor frontend and docs - Build frontend (
npm run build) and docs (npm run build) - 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'],
}
Documentation link broken (HTTPS → HTTP:3000)
Symptom: "View Documentation" link becomes https://tradeentry.in:3000 which fails because port 3000 is HTTP only.
Fix: Two parts:
Home.jsxuseshttp://explicitly for docs link (notwindow.location.protocol)docs.tradeentry.innginx 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
| Task | Command |
|---|---|
| Check all services | sudo systemctl status tradeentry nginx docker |
| View app logs | sudo journalctl -u tradeentry -f |
| Restart app | sudo systemctl restart tradeentry |
| Check DB rows | PGPASSWORD=tedb psql -h 127.0.0.1 -U postgres -d tedb -c "SELECT COUNT(*) FROM scr_nseeq_eod;" |
| Reload nginx | sudo nginx -t && sudo systemctl reload nginx |
| Kill ports manually | fuser -k 8000/tcp 5173/tcp 3000/tcp |
| Git pull + restart | cd ~/tradeentry && git pull && sudo systemctl restart tradeentry |
| Check Docker | docker ps |
| Start Docker DB | docker start postgres-db |
| Live rebuild | APP_ENV=VPS uv run app.py |
| Skip rebuild | APP_ENV=VPS uv run app.py --no-build |
Last updated: 2026-05-25