Deploy Turborepo to VPS: Complete Monorepo Deployment Guide
Deploy a full Turborepo monorepo — frontend, API, and shared packages — to your own VPS with PM2 process management, Nginx reverse proxy, free SSL, and automated CI/CD via GitHub Actions. No Vercel required.
Turborepo project structure
A typical Turborepo monorepo splits code into apps (deployable) and packages (shared libraries). Here is the structure this guide follows:
my-turborepo/ ├── apps/ │ ├── web/ # Next.js frontend (port 3000) │ └── api/ # Express API server (port 4000) ├── packages/ │ ├── shared/ # Shared types, utils, constants │ └── ui/ # Shared React component library ├── turbo.json # Turborepo pipeline config ├── package.json # Root workspace config └── package-lock.json
The root package.json defines npm workspaces. Each app and package has its own package.json with dependencies and scripts. Turborepo orchestrates builds based on the dependency graph.
What you need
| Spec | Minimum | Recommended |
|---|---|---|
| CPU | 2 vCPU | 4 vCPU |
| RAM | 2 GB | 4 GB |
| Storage | 30 GB SSD | 50 GB SSD |
| Monthly cost | ~$10/mo | ~$18/mo |
Why deploy a monorepo to your own VPS?
Vercel supports Turborepo natively, but monorepo pricing adds up fast. Each app counts as a separate project, build minutes multiply, and bandwidth limits apply per-project. A $12/month VPS can run your entire monorepo — frontend, API, background workers — with no per-project billing.
You also get full control: shared environment variables across apps, inter-service communication over localhost (zero latency), and a single deployment pipeline for your entire codebase.
Configure turbo.json
The turbo.json file at the monorepo root defines your build pipeline. Here is a production-ready configuration:
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^build"]
},
"type-check": {
"dependsOn": ["^build"]
}
}
}The "dependsOn": ["^build"] line means Turborepo builds shared packages first, then apps that depend on them. The outputs array tells Turborepo what to cache — .next/** for Next.js and dist/** for compiled packages.
1Prepare your VPS
SSH into your server and install the required tools. If you already have Node.js and PM2, skip ahead.
# Update system packages sudo apt update && sudo apt upgrade -y # Install Node.js 20 via NodeSource curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt install -y nodejs # Verify versions node -v # v20.x.x npm -v # 10.x.x # Install PM2 globally sudo npm install -g pm2 # Install Nginx sudo apt install -y nginx sudo systemctl enable nginx
2Clone and install dependencies
Clone the entire monorepo. Running npm install at the root handles all workspaces automatically.
# Clone the monorepo cd /var/www git clone https://github.com/your-org/my-turborepo.git cd my-turborepo # Install all workspace dependencies from root npm install # This installs deps for: # - apps/web # - apps/api # - packages/shared # - packages/ui # npm workspaces handles hoisting and symlinking
Do not run npm install inside individual workspace folders. The root lockfile manages all dependencies — installing inside a workspace can create conflicting lockfiles and break hoisting.
3Build with turbo filters
Use the --filter flag to build specific apps. Turborepo resolves the dependency graph and builds shared packages first.
# Build the Next.js frontend (and its dependencies) npx turbo run build --filter=web # Build the API server (and its dependencies) npx turbo run build --filter=api # Or build everything at once npx turbo run build
When you run --filter=web, Turborepo detects that web depends on packages/shared and packages/ui, builds those first, then builds the web app. Subsequent builds are cached — if shared packages haven't changed, they are skipped.
4Run apps with PM2
Create an ecosystem.config.js at the monorepo root to manage both apps as PM2 processes:
// ecosystem.config.js
module.exports = {
apps: [
{
name: "web",
cwd: "./apps/web",
script: "node_modules/.bin/next",
args: "start -p 3000",
env: {
NODE_ENV: "production",
PORT: 3000,
},
instances: 1,
autorestart: true,
max_memory_restart: "512M",
},
{
name: "api",
cwd: "./apps/api",
script: "dist/index.js",
env: {
NODE_ENV: "production",
PORT: 4000,
},
instances: 1,
autorestart: true,
max_memory_restart: "256M",
},
],
};# Start both apps pm2 start ecosystem.config.js # Check status pm2 status # View logs for a specific app pm2 logs web pm2 logs api # Save process list so PM2 restarts them on reboot pm2 save pm2 startup
The cwd field is critical — it tells PM2 to run each process from its workspace directory so relative paths resolve correctly. The web app uses Next.js's built-in next start, while the API runs the compiled dist/index.js.
5Configure Nginx for multiple apps
Set up separate Nginx server blocks so app.domain.com routes to the frontend (port 3000) and api.domain.com routes to the API (port 4000).
# /etc/nginx/sites-available/app.domain.com
server {
listen 80;
server_name app.domain.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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_cache_bypass $http_upgrade;
}
}
# /etc/nginx/sites-available/api.domain.com
server {
listen 80;
server_name api.domain.com;
location / {
proxy_pass http://127.0.0.1:4000;
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 both sites sudo ln -s /etc/nginx/sites-available/app.domain.com /etc/nginx/sites-enabled/ sudo ln -s /etc/nginx/sites-available/api.domain.com /etc/nginx/sites-enabled/ # Test config and reload sudo nginx -t sudo systemctl reload nginx
6SSL for both subdomains
Use Certbot to issue free Let's Encrypt certificates for both subdomains in a single command:
# Install Certbot sudo apt install -y certbot python3-certbot-nginx # Issue SSL for both subdomains at once sudo certbot --nginx -d app.domain.com -d api.domain.com # Certbot automatically: # - Obtains certificates # - Updates Nginx configs to listen on 443 # - Adds HTTP -> HTTPS redirects # - Sets up auto-renewal via systemd timer # Verify auto-renewal works sudo certbot renew --dry-run
Make sure both DNS A records point to your VPS IP before running Certbot. The verification process requires port 80 to be accessible.
7CI/CD with GitHub Actions
Automate deployments on every push to main. This workflow uses Turborepo's remote cache for faster builds:
# .github/workflows/deploy.yml
name: Deploy Monorepo
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Build with Turborepo
run: npx turbo run build
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
- name: Deploy to VPS
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
cd /var/www/my-turborepo
git pull origin main
npm ci
npx turbo run build
pm2 restart ecosystem.config.js
pm2 saveStore your VPS credentials as GitHub repository secrets: VPS_HOST, VPS_USER, and VPS_SSH_KEY. For Turborepo remote caching, add TURBO_TOKEN and TURBO_TEAM from your Vercel account or self-hosted cache.
Common issues
Ensure your root package.json has the workspaces field: "workspaces": ["apps/*", "packages/*"]. Also verify each package has a "name" field that matches what importing apps reference.
Turborepo caches aggressively. Run npx turbo run build --force to bypass the cache, or ensure your turbo.json has "dependsOn": ["^build"] so that shared packages rebuild when changed.
The ^build syntax in dependsOn means "build dependencies first". Without the caret (^), Turborepo won't respect the dependency graph. Check that turbo.json uses "dependsOn": ["^build"].
The cwd field in ecosystem.config.js must point to the workspace directory (e.g. ./apps/web). PM2 resolves the script path relative to cwd. Double-check with: ls apps/web/node_modules/.bin/next
Monorepo builds are memory-intensive. Add swap space: sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile. Or build apps sequentially with separate --filter commands.
Skip the manual work with DeployWise
The steps above work, but maintaining Nginx configs, PM2 processes, SSL renewals, and deployment scripts across a monorepo is real operational overhead. DeployWise automates the entire pipeline:
Connect your GitHub monorepo, point to your VPS, and DeployWise handles the rest — for free.
Deploy your monorepo in minutes
Sign in with GitHub, add your VPS, and deploy your entire Turborepo monorepo — frontend, API, and all — for free.
Open DeployWise DashboardFrequently Asked Questions
Can I deploy a Turborepo monorepo to a VPS without Vercel?+
Yes. Turborepo is a build tool, not a hosting platform. You can build each workspace app with turbo run build --filter and deploy to any VPS using PM2, Nginx, and SSL.
How do I build only one app in a Turborepo monorepo?+
Use the --filter flag: npx turbo run build --filter=web. Turborepo automatically builds all dependencies (shared packages) that the filtered app requires.
Do I need to install dependencies in each workspace separately?+
No. Running npm install at the monorepo root installs dependencies for all workspaces. npm workspaces and Turborepo handle the hoisting and linking.
How much RAM does a Turborepo monorepo need on a VPS?+
For a typical two-app monorepo (Next.js frontend + Express API), 2 GB RAM is comfortable for builds and runtime. Add 1 GB swap as a safety net for large builds.
Can I use Turborepo remote caching on a VPS?+
Yes. You can use Vercel remote cache (free tier available) or self-host a cache server. Set TURBO_TOKEN and TURBO_TEAM environment variables, or use --remote-only flag.
How do I handle shared packages in a monorepo deployment?+
Shared packages (packages/shared, packages/ui) are built automatically when you build a dependent app with --filter. Turborepo resolves the dependency graph and builds in the correct order.