DeployWise
HomeGuidesDeploy Turborepo to VPS
TurborepoMonorepoVPSPM2NginxGitHub Actions

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.

12 min read
Published March 2026

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

A VPS running Ubuntu 22.04+ (Hetzner, DigitalOcean, Vultr, etc.)
SSH access with root or sudo privileges
A Turborepo monorepo hosted on GitHub
Two domain names or subdomains (e.g. app.domain.com, api.domain.com)
Node.js 18+ and npm 8+ (we install these on the VPS)
Basic terminal/SSH familiarity
SpecMinimumRecommended
CPU2 vCPU4 vCPU
RAM2 GB4 GB
Storage30 GB SSD50 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 save

Store 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

Workspace packages not found during build

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.

Shared package changes not reflected in app build

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.

Build order is wrong — app builds before its dependencies

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"].

PM2 can't find the start script in workspace apps

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

Out of memory during monorepo build

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:

Detects Turborepo monorepos and lists each app as a deployable project
Builds individual apps with --filter automatically
Manages PM2 processes per workspace — start, restart, logs
Configures Nginx and issues SSL for each app's domain
Auto-deploys on git push with zero-downtime restarts
Shared environment variables across workspace apps

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 Dashboard

Frequently 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.

Related guides