This guide covers deploying your TailStack Core monorepo to production, including building both frontend and backend, configuring environment variables, and deployment strategies.
Pre-Deployment Checklist
Before deploying, ensure your application is production-ready:
Environment Variables
Configure production environment variables for both frontend and backend.
Build Testing
Test production builds locally to catch issues early.
Security Review
Review CORS settings, API keys, and sensitive data handling.
Performance
Ensure clustering is enabled and optimized for production.
Building for Production
Backend Build
The backend TypeScript code must be compiled to JavaScript:
Build Command
Output Structure
Start Production Server
cd packages/core/source/Server
pnpm build
This runs the build script from package.json:8: {
"scripts" : {
"build" : "tsc"
}
}
The TypeScript compiler outputs JavaScript to the dist/ directory. After building, you’ll have: packages/core/source/Server/
├── dist/ # Compiled JavaScript
│ ├── app.js
│ ├── server.js
│ ├── routes/
│ ├── controller/
│ ├── services/
│ └── ...
├── src/ # Source TypeScript
└── package.json
From package.json:9: {
"scripts" : {
"start" : "node dist/server.js"
}
}
Frontend Build
The frontend must be built for production using Vite:
Build Command
Output
Preview Build
cd packages/core/source/frontend
pnpm build
From package.json:8: {
"scripts" : {
"build" : "tsc -b && vite build"
}
}
This:
Compiles TypeScript (tsc -b)
Bundles with Vite (vite build)
Vite outputs optimized static files to dist/: packages/core/source/frontend/
├── dist/ # Production build
│ ├── index.html
│ ├── assets/
│ │ ├── index-[hash].js
│ │ ├── index-[hash].css
│ │ └── ...
│ └── ...
└── src/
Test the production build locally: Vite serves the dist/ folder at http://localhost:4173
Build Entire Monorepo
From the root packages/core/ directory:
# Build both frontend and backend
pnpm --filter ./source/frontend build
pnpm --filter ./source/Server build
Or create a combined script in packages/core/package.json:
{
"scripts" : {
"build" : "pnpm --filter ./source/frontend build && pnpm --filter ./source/Server build" ,
"build:frontend" : "pnpm --filter ./source/frontend build" ,
"build:backend" : "pnpm --filter ./source/Server build"
}
}
Environment Variables
Backend Environment Variables
Create packages/core/source/Server/.env.production:
# Server Configuration
PORT = 5000
NODE_ENV = production
# CORS - Set to your frontend domain
CORS_ORIGIN = https://your-app.com
# Cluster Configuration
WORKERS = 0 # Auto-detect CPU cores for maximum performance
# API Keys (use secrets manager in production)
WEATHER_API_KEY = your-production-api-key
# Database
DATABASE_URL = postgresql://user:pass@host:5432/dbname
# Optional: Rate Limiting
RATE_LIMIT_WINDOW_MS = 900000
RATE_LIMIT_MAX_REQUESTS = 100
Never commit .env files with production secrets. Use environment variable management from your hosting provider or a secrets manager like AWS Secrets Manager, HashiCorp Vault, or Doppler.
Frontend Environment Variables
Create packages/core/source/frontend/.env.production:
# API Configuration - Set to your backend URL
VITE_API_BASE_URL = https://api.your-app.com
# Optional: Analytics
VITE_ANALYTICS_ID = your-analytics-id
# Optional: Feature Flags
VITE_ENABLE_FEATURE_X = true
Vite only exposes variables prefixed with VITE_ to the client. These are read in src/config/api.ts:2:
export const API_CONFIG = {
baseURL: import . meta . env . VITE_API_BASE_URL || 'http://localhost:5000' ,
endpoints: {
weather: {
byLocation: '/api/weather/location' ,
byCoordinates: '/api/weather/coordinates' ,
},
},
};
Deployment Strategies
Strategy 1: Separate Hosting (Recommended)
Deploy frontend and backend separately for better scalability:
Frontend (Vercel)
Backend (Railway)
Backend (Render)
Frontend (Netlify)
Deploy the frontend to Vercel:
Connect Repository
Push your code to GitHub/GitLab/Bitbucket
Import project in Vercel
Configure Build Settings
# Root Directory
packages/core/source/frontend
# Build Command
pnpm build
# Output Directory
dist
# Install Command
pnpm install
Environment Variables
Add in Vercel dashboard:
VITE_API_BASE_URL=https://api.your-app.com
Deploy
Vercel auto-deploys on git push
Frontend available at https://your-app.vercel.app
Deploy the backend to Railway:
Create New Project
Connect GitHub repository
Railway auto-detects Node.js
Configure
# Root Directory
packages/core/source/Server
# Build Command
pnpm build
# Start Command
pnpm start
Environment Variables
Add in Railway dashboard:
PORT=5000
NODE_ENV=production
CORS_ORIGIN=https://your-app.vercel.app
WORKERS=0
Deploy
Railway auto-deploys on git push
Backend available at https://your-app.up.railway.app
Deploy the backend to Render:
Create Web Service
Connect repository
Select Node runtime
Configure
# Root Directory
packages/core/source/Server
# Build Command
pnpm install && pnpm build
# Start Command
pnpm start
Environment Variables
PORT=5000
NODE_ENV=production
CORS_ORIGIN=https://your-app.vercel.app
WORKERS=0
Health Check
Set path to /health
Deploy the frontend to Netlify:
Connect Repository
Build Settings
# Base Directory
packages/core/source/frontend
# Build Command
pnpm build
# Publish Directory
dist
Environment Variables
VITE_API_BASE_URL=https://api.your-app.com
Redirects for SPA
Create packages/core/source/frontend/public/_redirects:
Strategy 2: Single Server (VPS)
Deploy both frontend and backend on a single server:
Setup Server
Use a VPS provider (DigitalOcean, Linode, AWS EC2): # Update system
sudo apt update && sudo apt upgrade -y
# Install Node.js 20+
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
# Install pnpm
npm install -g pnpm
# Install nginx
sudo apt install -y nginx
Deploy Code
# Clone repository
git clone https://github.com/your-repo/tailstack.git
cd tailstack
# Install dependencies
pnpm install
# Build frontend and backend
cd packages/core
pnpm --filter ./source/frontend build
pnpm --filter ./source/Server build
Configure Nginx
Create /etc/nginx/sites-available/tailstack: server {
listen 80 ;
server_name your-domain.com;
# Frontend - serve static files
location / {
root /path/to/tailstack/packages/core/source/frontend/dist;
try_files $ uri $ uri / /index.html;
}
# Backend - proxy to Node.js
location /api {
proxy_pass http://localhost:5000;
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 ;
}
# Health check
location /health {
proxy_pass http://localhost:5000;
}
}
Enable and restart: sudo ln -s /etc/nginx/sites-available/tailstack /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
Process Manager (PM2)
Use PM2 to keep the backend running: # Install PM2
npm install -g pm2
# Start backend
cd packages/core/source/Server
pm2 start dist/server.js --name tailstack-api
# Save PM2 configuration
pm2 save
pm2 startup
# Monitor
pm2 status
pm2 logs tailstack-api
SSL with Certbot
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d your-domain.com
Strategy 3: Docker Deployment
Containerize your application:
Backend Dockerfile
Frontend Dockerfile
docker-compose.yml
Create packages/core/source/Server/Dockerfile: FROM node:20-alpine AS builder
WORKDIR /app
# Install pnpm
RUN npm install -g pnpm
# Copy package files
COPY package.json pnpm-lock.yaml ./
# Install dependencies
RUN pnpm install --frozen-lockfile
# Copy source
COPY . .
# Build
RUN pnpm build
# Production image
FROM node:20-alpine
WORKDIR /app
RUN npm install -g pnpm
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --prod --frozen-lockfile
COPY --from=builder /app/dist ./dist
EXPOSE 5000
CMD [ "pnpm" , "start" ]
Create packages/core/source/frontend/Dockerfile: FROM node:20-alpine AS builder
WORKDIR /app
RUN npm install -g pnpm
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
# Serve with nginx
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
# Custom nginx config for SPA
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD [ "nginx" , "-g" , "daemon off;" ]
Create packages/core/source/frontend/nginx.conf: server {
listen 80 ;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $ uri $ uri / /index.html;
}
}
Create packages/core/docker-compose.yml: version : '3.8'
services :
backend :
build :
context : ./source/Server
dockerfile : Dockerfile
ports :
- "5000:5000"
environment :
- NODE_ENV=production
- PORT=5000
- CORS_ORIGIN=http://localhost:3000
- WORKERS=0
restart : unless-stopped
frontend :
build :
context : ./source/frontend
dockerfile : Dockerfile
ports :
- "3000:80"
environment :
- VITE_API_BASE_URL=http://localhost:5000
depends_on :
- backend
restart : unless-stopped
Deploy:
Cluster Configuration
The backend uses Node.js clustering for production performance:
packages/core/source/Server/src/cluster/index.ts:1
export const initializeCluster = ( workerCallback : () => void ) => {
if ( cluster . isPrimary ) {
const numCPUs = config . workers || availableParallelism ();
console . log ( `⚙️ Spawning ${ numCPUs } workers for maximum performance...` );
for ( let i = 0 ; i < numCPUs ; i ++ ) {
cluster . fork ();
}
// Auto-restart failed workers
cluster . on ( 'exit' , ( worker , code , signal ) => {
console . log ( `⚠️ Worker ${ worker . process . pid } died` );
setTimeout (() => cluster . fork (), CLUSTER_CONFIG . RESTART_DELAY );
});
} else {
workerCallback ();
}
};
Cluster Environment Variables
# Auto-detect CPU cores (recommended for production)
WORKERS = 0
# Manual control (useful for limited resources)
WORKERS = 2
# Single worker for debugging
WORKERS = 1
Setting WORKERS=0 auto-detects CPU cores using availableParallelism(). On a 4-core server, this spawns 4 worker processes for optimal performance.
CORS Configuration
Update CORS for production:
packages/core/source/Server/src/middlewares/cors.ts
import cors from 'cors' ;
import { config } from '../config' ;
export const corsMiddleware = cors ({
origin: config . corsOrigin , // Set via CORS_ORIGIN env var
credentials: true ,
});
Production .env:
# Single domain
CORS_ORIGIN = https://your-app.com
# Multiple domains (modify middleware)
CORS_ORIGINS = https://app1.com,https://app2.com
For multiple origins:
const allowedOrigins = process . env . CORS_ORIGINS ?. split ( ',' ) || [];
export const corsMiddleware = cors ({
origin : ( origin , callback ) => {
if ( ! origin || allowedOrigins . includes ( origin )) {
callback ( null , true );
} else {
callback ( new Error ( 'Not allowed by CORS' ));
}
},
credentials: true ,
});
Health Checks
The backend includes a health check endpoint at /health:
packages/core/source/Server/src/app.ts:18
app . get ( '/health' , ( req , res ) => {
res . json ({ status: 'ok' , message: 'Server is running' });
});
Use this for:
Load balancer health checks
Monitoring services
Container orchestration (Kubernetes liveness probes)
Monitoring
Add monitoring to your production deployment:
// src/app.ts
app . get ( '/health' , ( req , res ) => {
res . json ({
status: 'ok' ,
message: 'Server is running' ,
uptime: process . uptime (),
timestamp: new Date (). toISOString (),
memory: process . memoryUsage (),
});
});
Troubleshooting
Frontend can't connect to backend
Check VITE_API_BASE_URL is set to your backend URL
Verify CORS is configured: CORS_ORIGIN matches your frontend domain
Check browser console for CORS errors
Test backend health check: curl https://api.your-app.com/health
Backend crashes in production
Check logs: pm2 logs or platform logs
Verify all environment variables are set
Check NODE_ENV=production
Ensure WORKERS is configured correctly
Review cluster restart logs
Verify Node.js version (20+)
Clear node_modules: rm -rf node_modules && pnpm install
Check TypeScript errors: pnpm tsc --noEmit
Review build logs for specific errors
Reduce workers: WORKERS=2
Add memory limit to PM2: pm2 start dist/server.js --max-memory-restart 500M
Monitor with pm2 monit
Consider vertical scaling (more RAM)
Next Steps
CI/CD Setup Set up automated deployments with GitHub Actions or GitLab CI
Database Integration Add PostgreSQL, MongoDB, or your preferred database
Caching Implement Redis for session storage and API caching
Monitoring Add Sentry for error tracking and analytics