Monorepo Deployment Strategies with DeployHQ: Nx, Turborepo, and Beyond

Devops & Infrastructure, Node, and Tips & Tricks

Monorepo Deployment Strategies with DeployHQ: Nx, Turborepo, and Beyond

Monorepos have transformed how teams manage large codebases, enabling shared code, atomic commits, and unified tooling. But deploying from a monorepo presents unique challenges: selective deployments, dependency management, and build optimization. In this guide, you'll learn strategies for deploying monorepos with DeployHQ using Nx, Turborepo, and other tools.

Why Monorepos?

Monorepos consolidate multiple projects into a single repository, offering significant benefits:

  • Atomic commits across related packages
  • Shared code without npm publishing
  • Unified CI/CD pipelines
  • Consistent tooling and standards
  • Simplified dependency management
flowchart TD
    subgraph "Polyrepo"
        R1[repo-frontend]
        R2[repo-backend]
        R3[repo-shared]
        R1 -.-> |npm install| R3
        R2 -.-> |npm install| R3
    end

    subgraph "Monorepo"
        M[monorepo]
        M --> F[packages/frontend]
        M --> B[packages/backend]
        M --> S[packages/shared]
        F --> |direct import| S
        B --> |direct import| S
    end

Monorepo Structure

A typical monorepo structure looks like this:

my-monorepo/
├── apps/
│   ├── web/              # Frontend application
│   │   ├── src/
│   │   └── package.json
│   ├── api/              # Backend API
│   │   ├── src/
│   │   └── package.json
│   └── admin/            # Admin dashboard
│       ├── src/
│       └── package.json
├── packages/
│   ├── ui/               # Shared UI components
│   │   ├── src/
│   │   └── package.json
│   ├── utils/            # Shared utilities
│   │   └── package.json
│   └── config/           # Shared configuration
│       └── package.json
├── package.json          # Root package.json
├── nx.json               # Nx configuration
├── turbo.json            # Turborepo configuration
└── pnpm-workspace.yaml   # Workspace definition

Nx: Enterprise-Scale Monorepos

Nx provides powerful features for large monorepos, including computation caching, affected commands, and task orchestration.

Nx Project Setup

// nx.json
{
  "npmScope": "mycompany",
  "tasksRunnerOptions": {
    "default": {
      "runner": "nx/tasks-runners/default",
      "options": {
        "cacheableOperations": ["build", "test", "lint"]
      }
    }
  },
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["production", "^production"]
    },
    "test": {
      "inputs": ["default", "^production"]
    }
  },
  "namedInputs": {
    "default": ["{projectRoot}/**/*", "sharedGlobals"],
    "production": ["default", "!{projectRoot}/**/*.spec.ts"],
    "sharedGlobals": []
  }
}

App Configuration

// apps/web/project.json
{
  "name": "web",
  "sourceRoot": "apps/web/src",
  "projectType": "application",
  "targets": {
    "build": {
      "executor": "@nx/vite:build",
      "outputs": ["{options.outputPath}"],
      "options": {
        "outputPath": "dist/apps/web"
      }
    },
    "deploy": {
      "executor": "nx:run-commands",
      "options": {
        "command": "deployhq-deploy web"
      },
      "dependsOn": ["build"]
    }
  }
}

DeployHQ Build Commands with Nx

# Install dependencies
pnpm install --frozen-lockfile

# Determine affected projects
AFFECTED=$(npx nx affected:apps --plain --base=origin/main~1 --head=HEAD)

# Build only affected apps
if [ -n "$AFFECTED" ]; then
    echo "Building affected apps: $AFFECTED"
    npx nx affected:build --base=origin/main~1 --head=HEAD
else
    echo "No apps affected, skipping build"
fi

Selective Deployment Script

#!/bin/bash
# .deployhq/deploy.sh

set -e

# Get list of affected apps
AFFECTED_APPS=$(npx nx affected:apps --plain --base=origin/main~1 --head=HEAD)

# Deploy each affected app
for app in $AFFECTED_APPS; do
    echo "=== Deploying $app ==="

    case $app in
        web)
            rsync -avz dist/apps/web/ user@web-server:/var/www/web/
            ;;
        api)
            rsync -avz dist/apps/api/ user@api-server:/var/www/api/
            ssh user@api-server "pm2 restart api"
            ;;
        admin)
            rsync -avz dist/apps/admin/ user@admin-server:/var/www/admin/
            ;;
        *)
            echo "Unknown app: $app"
            ;;
    esac
done

echo "=== Deployment complete ==="

For more on build pipelines, see our dedicated guide.

Turborepo: Fast Monorepo Builds

Turborepo focuses on build speed through intelligent caching and parallel execution.

Turborepo Configuration

// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
    },
    "test": {
      "dependsOn": ["build"],
      "inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
    },
    "lint": {
      "outputs": []
    },
    "deploy": {
      "dependsOn": ["build", "test", "lint"],
      "cache": false
    }
  }
}

DeployHQ Build Commands with Turborepo

# Install dependencies
pnpm install --frozen-lockfile

# Run build with Turborepo caching
npx turbo run build --filter=...@mycompany/web

# Or build all changed since last deploy
npx turbo run build --filter=[origin/main~1]

Remote Caching for CI

Enable remote caching to speed up builds across deployments:

# Enable Turborepo remote caching
npx turbo login
npx turbo link

# Build with remote cache
TURBO_TEAM="myteam" TURBO_TOKEN="$TURBO_TOKEN" npx turbo run build

Package Manager Considerations

Monorepos work best with modern package managers:

pnpm Workspaces

# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'
# Install with pnpm
pnpm install --frozen-lockfile

# Run command in specific package
pnpm --filter @mycompany/web build

# Run command in all packages
pnpm -r build

npm Workspaces

// package.json
{
  "workspaces": [
    "apps/*",
    "packages/*"
  ]
}

For a comparison of package managers, see our guide on choosing the right package manager.

Deployment Strategies

Strategy 1: Deploy All Apps

Simple but potentially wasteful:

# Build everything
npx turbo run build

# Deploy all apps
for app in apps/*; do
    deploy_app $app
done

Strategy 2: Deploy Affected Only

Efficient for large monorepos:

flowchart LR
    A[Code Change] --> B{What Changed?}
    B -->|packages/utils| C[Rebuild All Dependents]
    B -->|apps/web| D[Rebuild web Only]
    B -->|apps/api| E[Rebuild api Only]
    C --> F[Deploy Affected]
    D --> F
    E --> F
# Get affected apps
AFFECTED=$(npx nx affected:apps --plain)

# Build and deploy only affected
for app in $AFFECTED; do
    npx nx build $app
    deploy_app $app
done

Strategy 3: Per-App Projects in DeployHQ

Create separate DeployHQ projects for each app, each with filtered build commands:

# Project: Web App
npx turbo run build --filter=@mycompany/web
# Deploy dist/apps/web

# Project: API
npx turbo run build --filter=@mycompany/api
# Deploy dist/apps/api

This is where DeployHQ shines: give each app its own project with per-project build pipelines and filtered build commands, so a change to apps/web never redeploys apps/api. Deploy your first monorepo app free.

Environment Configuration

Manage environment variables across apps:

// packages/config/src/env.ts
export function getEnvConfig(appName: string) {
  const common = {
    NODE_ENV: process.env.NODE_ENV || 'development',
    LOG_LEVEL: process.env.LOG_LEVEL || 'info',
  };

  const appSpecific = {
    web: {
      API_URL: process.env.WEB_API_URL,
      PUBLIC_URL: process.env.WEB_PUBLIC_URL,
    },
    api: {
      DATABASE_URL: process.env.API_DATABASE_URL,
      REDIS_URL: process.env.API_REDIS_URL,
    },
  };

  return {
    ...common,
    ...appSpecific[appName],
  };
}

For managing environments, see managing multiple environments with DeployHQ.

Dependency Graph Visualization

Understanding your dependency graph helps optimize deployments:

# Generate dependency graph with Nx
npx nx graph

# Export as JSON
npx nx graph --file=graph.json
flowchart TD
    subgraph "Apps"
        web[web]
        api[api]
        admin[admin]
    end

    subgraph "Packages"
        ui[ui]
        utils[utils]
        config[config]
    end

    web --> ui
    web --> utils
    web --> config
    admin --> ui
    admin --> utils
    api --> utils
    api --> config
    ui --> utils

Testing in Monorepos

Run tests efficiently:

# Test only affected packages
npx nx affected:test --base=origin/main

# Test specific app and its dependencies
npx turbo run test --filter=@mycompany/web...

# Test all packages in parallel
npx turbo run test --parallel

Best Practices Summary

  1. Use affected commands to build/deploy only changed code
  2. Enable caching (local and remote) for faster builds
  3. Structure dependencies clearly between packages
  4. Use workspace protocols for internal dependencies
  5. Configure clear pipelines for build, test, and deploy
  6. Separate concerns between apps and shared packages
  7. Version together or use independent versioning thoughtfully
  8. Monitor build times and optimize bottlenecks

Getting Started

Ready to deploy your monorepo? Here's your checklist:

  1. Choose your monorepo tool (Nx or Turborepo)
  2. Set up workspace configuration
  3. Configure DeployHQ build commands
  4. Implement affected/filtered builds
  5. Set up per-app deployments

New to automated deploys? Start with our guide to building a CI/CD pipeline from scratch, then layer in the monorepo filters above.

For Node.js-specific guidance, see our guide on Node application servers.


Questions about monorepo deployments? Reach out to support@deployhq.com or follow @deployhq for deployment tips.