This repository contains:
frontend/: Next.js 14 applicationbackend/: NestJS API with Prisma- PostgreSQL as the database
This guide prepares the project for a standard Linux VPS deployment on Octanium without Docker.
- Frontend framework: Next.js 14 App Router
- Backend framework: NestJS 10
- ORM: Prisma 5
- Database: PostgreSQL
- File uploads: stored on disk in
UPLOAD_DIR - Auth: JWT access + refresh tokens
cd backend
npm run build
npm run start:prod
npm run prisma:migrate
npm run prisma:seed:prodcd frontend
npm run build
npm run startThe repo has been updated to be VPS-friendly:
- upload storage now uses a shared env-driven absolute path
- backend now binds on
0.0.0.0 - backend CORS supports
CORS_ORIGINS - frontend API fallback now uses
/apiinstead of leakinglocalhost - frontend asset URLs use a shared runtime helper
- env templates were sanitized
- PM2 config was added in
ecosystem.config.js - README now documents no-Docker deployment
Create these files on the VPS:
/var/www/pharmapp/shared/config/backend.env/var/www/pharmapp/shared/config/frontend.env
NODE_ENV=production
PORT=4000
DATABASE_URL=postgresql://pharmaflow:STRONG_DB_PASSWORD@127.0.0.1:5432/pharmaflow?schema=public
FRONTEND_URL=https://app.example.com
CORS_ORIGINS=https://app.example.com,https://www.app.example.com
UPLOAD_DIR=/var/www/pharmapp/shared/uploads
JWT_SECRET=GENERATE_A_LONG_RANDOM_SECRET
JWT_REFRESH_SECRET=GENERATE_ANOTHER_LONG_RANDOM_SECRET
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d
GMAIL_USER=
GMAIL_PASS=NEXT_PUBLIC_API_URL=https://app.example.com/api
INTERNAL_API_URL=http://127.0.0.1:4000/apiGenerate secrets:
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"/var/www/pharmapp/
current/
backend/
frontend/
ecosystem.config.js
shared/
config/
backend.env
frontend.env
uploads/
These commands assume Ubuntu/Debian.
sudo apt update
sudo apt install -y curl git unzip build-essential nginx postgresql postgresql-contribcurl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
node -v
npm -vsudo npm install -g pm2
pm2 -vIf you are deploying from a ZIP:
mkdir -p /var/www/pharmapp
cd /var/www/pharmapp
mkdir -p current shared/config shared/uploadsUpload the ZIP, then:
cd /var/www/pharmapp
unzip pharmapp.zip -d release
cp -r release/backend current/
cp -r release/frontend current/
cp release/ecosystem.config.js current/
rm -rf releaseIf the ZIP extracts into a top-level folder, adjust the cp source path accordingly.
sudo -u postgres psqlInside psql:
CREATE USER pharmaflow WITH PASSWORD 'STRONG_DB_PASSWORD';
CREATE DATABASE pharmaflow OWNER pharmaflow;
GRANT ALL PRIVILEGES ON DATABASE pharmaflow TO pharmaflow;
\qFrom pgAdmin on your local machine:
- Right-click the source database.
- Choose
Backup... - Format:
Plain - File name:
pharmaflow.sql - Click
Backup
If you prefer pg_dump directly:
pg_dump -h localhost -U postgres -d pharma_db -F p -f pharmaflow.sqlCopy pharmaflow.sql to the server, then run:
psql "postgresql://pharmaflow:STRONG_DB_PASSWORD@127.0.0.1:5432/pharmaflow" -f pharmaflow.sqlAfter import, apply Prisma migrations so the schema matches the codebase:
cd /var/www/pharmapp/current/backend
cp /var/www/pharmapp/shared/config/backend.env .env
npm install
npx prisma migrate deployIf the imported database already contains all tables and migration history is missing, use this safer sequence:
cd /var/www/pharmapp/current/backend
cp /var/www/pharmapp/shared/config/backend.env .env
npm install
npx prisma db pull
npx prisma migrate statusIf migrate deploy complains because the production DB was created outside Prisma, stop there and reconcile migration history before forcing changes.
cd /var/www/pharmapp/current/backend
cp /var/www/pharmapp/shared/config/backend.env .env
npm ci
npm run build
npx prisma migrate deployOptional seed:
npm run prisma:seed:prodcd /var/www/pharmapp/current/frontend
cp /var/www/pharmapp/shared/config/frontend.env .env.production
npm ci
npm run buildFrom the project root on the server:
cd /var/www/pharmapp/current
pm2 start ecosystem.config.js
pm2 save
pm2 startup systemdUseful PM2 commands:
pm2 status
pm2 logs pharmapp-backend
pm2 logs pharmapp-frontend
pm2 restart pharmapp-backend
pm2 restart pharmapp-frontend
pm2 reload allCreate /etc/nginx/sites-available/pharmapp:
server {
listen 80;
server_name app.example.com www.app.example.com;
client_max_body_size 25M;
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-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /api/ {
proxy_pass http://127.0.0.1:4000/api/;
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;
}
location /uploads/ {
proxy_pass http://127.0.0.1:4000/uploads/;
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;
}
}Enable it:
sudo ln -s /etc/nginx/sites-available/pharmapp /etc/nginx/sites-enabled/pharmapp
sudo nginx -t
sudo systemctl reload nginxIf you want HTTPS, add Certbot after HTTP is working:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d app.example.com -d www.app.example.com- Next.js frontend:
3000 - NestJS backend:
4000 - PostgreSQL:
5432
Keep PostgreSQL private to the server if possible. Prefer 127.0.0.1 instead of exposing it publicly.
sudo apt update
sudo apt install -y curl git unzip build-essential nginx postgresql postgresql-contrib
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
sudo npm install -g pm2
mkdir -p /var/www/pharmapp/current /var/www/pharmapp/shared/config /var/www/pharmapp/shared/uploads
# upload pharmapp.zip first
cd /var/www/pharmapp
unzip pharmapp.zip -d release
cp -r release/backend current/
cp -r release/frontend current/
cp release/ecosystem.config.js current/
nano /var/www/pharmapp/shared/config/backend.env
nano /var/www/pharmapp/shared/config/frontend.env
cd /var/www/pharmapp/current/backend
cp /var/www/pharmapp/shared/config/backend.env .env
npm ci
npm run build
npx prisma migrate deploy
cd /var/www/pharmapp/current/frontend
cp /var/www/pharmapp/shared/config/frontend.env .env.production
npm ci
npm run build
cd /var/www/pharmapp/current
pm2 start ecosystem.config.js
pm2 saveThen configure Nginx and test:
curl http://127.0.0.1:4000/api/health
curl -I http://127.0.0.1:3000
pm2 statuscurl http://127.0.0.1:4000/api/healthreturns200pm2 statusshows both appsonlinenpm run buildsucceeds in bothbackendandfrontend- uploads directory exists at
/var/www/pharmapp/shared/uploads NEXT_PUBLIC_API_URLpoints to the public domain, not localhostINTERNAL_API_URLpoints tohttp://127.0.0.1:4000/api- backend
.envhas the correctDATABASE_URL - Nginx proxies
/,/api/, and/uploads/
Cause:
- bad
DATABASE_URL - PostgreSQL not running
- wrong user/password
Fix:
sudo systemctl status postgresql
psql "postgresql://pharmaflow:STRONG_DB_PASSWORD@127.0.0.1:5432/pharmaflow"Cause:
- dependencies installed but Prisma client not regenerated
Fix:
cd /var/www/pharmapp/current/backend
npx prisma generate
npm run build
pm2 restart pharmapp-backendCause:
- wrong
NEXT_PUBLIC_API_URL - wrong Nginx
/api/proxy - backend process down
Fix:
pm2 logs pharmapp-backend
curl http://127.0.0.1:4000/api/healthCause:
UPLOAD_DIRdoes not exist- Nginx
/uploads/block missing - files stored in a different path than configured
Fix:
mkdir -p /var/www/pharmapp/shared/uploads
ls -la /var/www/pharmapp/shared/uploads
pm2 restart pharmapp-backendCause:
- missing
FRONTEND_URLorCORS_ORIGINS
Fix:
FRONTEND_URL=https://app.example.com
CORS_ORIGINS=https://app.example.com,https://www.app.example.comThen:
pm2 restart pharmapp-backendBecause the database currently lives locally and is managed with pgAdmin:
- export it first as a plain SQL dump
- import it on the VPS PostgreSQL instance
- only then run
npx prisma migrate deploy
That order preserves your existing data while still aligning production with Prisma migrations.