@digitalocean/mcp
Version:
DigitalOcean MCP Implementation,
1,416 lines (1,128 loc) ⢠74.8 kB
Markdown
# Vercel to App Platform Migration Instructions (v3)
š¦ **Version 3 Updates:**
- **NEW: Step 1.5 - Monorepo Dependency Consolidation** - Critical section on how App Platform handles monorepo dependencies
- **KEY INSIGHT:** App Platform only installs dependencies from root `package.json` in monorepos
- **PROACTIVE APPROACH:** Analyze and consolidate dependencies before deployment to avoid build failures
- **HYBRID STRATEGY:** Combine proactive analysis with fallback iterative fixing for best results
ā ļø **Important: This is a Starter Guide for AI Coding Assistants**
This guide helps AI coding assistants (Claude Code, Cursor, Gemini, etc.) collaborate with you to deploy applications to DigitalOcean App Platform.
**Constants:**
- **RETRY_LIMIT:** 3
**What this guide IS:**
- A structured workflow for AI-assisted deployment and migration
- A comprehensive checklist of steps and considerations
- Assisted error detection with fix suggestions (up to **RETRY_LIMIT** retry attempts)
- Truthful about limitations and manual steps required
- A starting point that handles common scenarios
**What this guide IS NOT:**
- A "magic migration button" that works without human oversight
- A guarantee of 1:1 feature parity with Vercel
- A replacement for understanding your application's architecture
- Suitable for highly complex, non-standard architectures without modifications
- Fully automated - you remain in control of all decisions
**Your role (the human):**
- Understand your application's architecture and requirements
- Make decisions about trade-offs and implementation approaches
- Verify deployment results and test functionality
- Implement code changes for unsupported features
- Monitor and validate production deployment
**AI assistant's role:**
- Guide you through the migration process step-by-step
- Automate repository and infrastructure setup tasks
- Detect common errors and suggest fixes
- Provide alternatives for unsupported features
- Generate configuration files and commands
**Success requires active collaboration between you and your AI assistant.**
---
## Supported Deployment Scenarios
This guide supports multiple scenarios:
1. **Migrating from existing Vercel deployment** - Moving a working Vercel app to App Platform (with or without Vercel CLI access)
2. **Deploying from GitHub repository** - App may be running on Vercel, but you just provide the repo to deploy to App Platform
3. **Deploying source code directly** - Getting source code working on App Platform (no Vercel deployment required)
4. **Deploying without repository access** - Creating new repository or deploying from local source
Follow these instructions carefully to ensure a smooth deployment.
## Initial Setup
### Determine Deployment Scenario
First, determine the user's scenario:
1. **Scenario A: Full Vercel Migration (with credentials)**
- User has an existing live Vercel app they want to migrate
- User has access to Vercel account/CLI
- Ask for the Vercel app name
- Will use Vercel CLI to pull environment variables and app configuration
- Will migrate domains and settings
2. **Scenario B: Repository-based Deployment (app may be on Vercel)**
- User provides a GitHub repository URL
- App may or may not be currently running on Vercel
- User says: "Here's my GitHub repo, deploy it to App Platform"
- No need for Vercel CLI access - work directly from repository
- Environment variables collected manually from .env.example or user input
3. **Scenario C: Source Code Only (no Vercel, no repo)**
- User has source code and wants to deploy to App Platform
- No Vercel deployment exists
- May or may not have existing GitHub repository
- Goal is to get the application working on App Platform
4. **Scenario D: No Repository Access**
- User wants to deploy without giving access to personal/private repo
- Can create new repository in their account using `gh` CLI
- Can fork existing repository
- Can deploy from local source (less recommended)
**For most migrations:** Scenario A or B are most common. Scenario B is simpler if user doesn't need/want to provide Vercel credentials.
### Verify Available Tools
**Primary Method: CLI Tools (Recommended)**
- **doctl CLI:** Primary method for App Platform operations (create apps, manage deployments, configure domains)
- **gh CLI:** For GitHub repository operations (create repo, fork, push code)
- **Vercel CLI (if migrating from Vercel):** For pulling environment variables and app details
**Alternative Method: MCP Servers (Optional)**
- **Vercel MCP server:** For project information (where available) - Optional
- **DigitalOcean MCP server:** For App Platform operations - Optional
- **Note:** CLI tools are preferred and don't require MCP server setup
**Verify available tools:**
- Check `doctl` is installed and authenticated: `doctl auth list`
- Check `gh` CLI is installed and authenticated: `gh auth status`
- If migrating from Vercel, check `vercel` CLI is installed: `vercel --version`
**Important Notes:**
- **CLI tools are the primary method** - MCP servers are optional
- Users don't need MCP servers - `doctl` and `gh` CLI are sufficient
- If user doesn't have a GitHub repository, we can create one using `gh` CLI
- If user doesn't want to give repo access, we can deploy from local source or create new repo
## Relevant App Platform Documentations
- [llms.txt](https://docs.digitalocean.com/products/app-platform/llms.txt)
- [nodejs specific buildpack instructions](https://docs.digitalocean.com/products/app-platform/reference/buildpacks/nodejs/)
- [App Spec reference](https://docs.digitalocean.com/products/app-platform/reference/app-spec/)
## Migration Process
### Step 0: Local Testing and Validation (Recommended)
**Before deploying to App Platform, test locally to catch issues early and avoid deployment wait time.**
1. **Verify local environment:**
- Check Node.js version: `node --version`
- Check package manager: `pnpm --version`, `npm --version`, or `yarn --version`
- Verify lockfile exists: `pnpm-lock.yaml`, `package-lock.json`, or `yarn.lock`
2. **Install dependencies locally:**
- Run: `pnpm install` (or `npm install` or `yarn install`)
- Verify all dependencies install successfully
- Check for any errors or warnings
3. **Test build locally:**
- Run: `pnpm build` (or `npm run build` or `yarn build`)
- Verify build completes successfully
- Check for build errors or warnings
- Verify build output directory exists (`.next` for Next.js, `dist` for others)
4. **Verify build artifacts:**
- Check build output directory structure
- Verify static files are generated
- Check for API routes in build output (if applicable)
- Verify middleware is compiled (if applicable)
5. **Check for hardcoded values:**
- Search codebase for hardcoded URLs or Vercel-specific code
- Verify environment variable usage
- Check for PORT usage (should use `process.env.PORT`)
6. **Validate database migrations (if applicable):**
- Check migration files exist
- Verify database configuration
- Test migration command (if database available)
### Step 1: Repository Setup
#### 1.1 Determine Repository Strategy
**Option A: Use Existing Repository (if user has one)**
- Check if current directory is a git repository: `git remote -v`
- If repository exists, verify it's accessible
- Use existing repository URL for App Platform
**Option B: Create New Repository (if user wants new repo)**
- Use `gh` CLI to create new repository: `gh repo create <repo-name> --private --source=. --push`
- Or: `gh repo create <repo-name> --public --source=. --push`
- Push current code to new repository
- Use new repository URL for App Platform
**Option C: Fork Existing Repository**
- Use `gh` CLI to fork: `gh repo fork <owner>/<repo-name> --clone=false`
- Add fork as remote: `git remote add fork https://github.com/<user>/<repo-name>.git`
- Push to fork: `git push fork main`
- Use forked repository URL for App Platform
**Option D: Deploy from Local Source (if user doesn't want repo access)**
- Can deploy directly from local source using `doctl apps create --spec app-spec.yaml`
- Note: This requires manual code updates - recommend using repository for easier updates
#### 1.2 Get App Information (If Migrating from Vercel)
**Only perform this step if user has existing Vercel deployment:**
1. **Create a local migration folder:**
- Pattern: `./vercel-migration/<vercel-app-name>-migration/`
- Example: `mkdir -p ./vercel-migration/my-app-migration`
- Navigate to it: `cd ./vercel-migration/<vercel-app-name>-migration/`
2. **Link to Vercel project (if not already linked):**
- Check if already linked: look for `.vercel` directory
- If NOT linked, run: `vercel link --project <your-project-name> -y`
- If already linked, skip this step to avoid double linking
3. **Pull environment variables from Vercel:**
- Ask the user which environment to pull from: production, development, preview, or other
- Run: `vercel env pull .env.<environment> --environment=<environment>`
- Examples:
- Production: `vercel env pull .env.production --environment=production`
- Development: `vercel env pull .env.development --environment=development`
- Preview: `vercel env pull .env.preview --environment=preview`
- Verify the file was created: `ls -la .env.<environment>`
- If unable to pull environment variables, leave them blank or empty when creating the App Platform app
4. **Get app information using Vercel CLI:**
- Use `vercel inspect <app-name>` or `vercel ls` to find the app
- Collect: App name, repository URL, deployment branch, build command, custom domains, etc.
5. **Show the user** what you found and confirm they want to proceed
- Include the list of domains that will need DNS updates (if migrating from Vercel)
- **Note:** Build and run commands will be validated in the next step by checking the repository
**If NOT migrating from Vercel (deploying source code directly):**
- Skip Vercel-specific steps
- Proceed directly to Step 1.5: Monorepo Dependency Consolidation (if applicable)
---
### Step 1.5: Monorepo Dependency Consolidation (CRITICAL for Monorepos)
ā ļø **IMPORTANT: App Platform Monorepo Behavior**
When deploying a monorepo with multiple components, App Platform has a specific behavior that you MUST understand:
**Key Insight:**
- App Platform installs dependencies **only from the root `package.json`**
- Each component's `source_dir` only affects where build/run commands execute
- Dependencies in component-specific `package.json` files are NOT automatically installed
- This means build tools like `tsx`, `typescript`, `pnpm`, etc. must be in root `package.json`
**Why This Matters:**
If your monorepo structure looks like this:
```
monorepo/
āāā package.json # Root - App Platform installs dependencies from HERE
āāā apps/
ā āāā dashboard/
ā ā āāā package.json # Component - dependencies NOT installed automatically
ā āāā api/
ā āāā package.json # Component - dependencies NOT installed automatically
```
And `apps/api/package.json` has `tsx` as a dependency, but root `package.json` doesn't, the API build will fail with "command not found: tsx".
---
#### 1.5.1: Analyze Monorepo Dependencies (Proactive Approach - Recommended)
**Before deploying**, analyze all component package.json files and consolidate dependencies:
1. **Detect if this is a monorepo:**
- Check for `pnpm-workspace.yaml`, `lerna.json`, `nx.json`
- Check if root `package.json` has "workspaces" field
- Look for multiple `package.json` files in subdirectories
2. **If monorepo detected, analyze component dependencies:**
```bash
# Read all component package.json files
# Extract dependencies and devDependencies
# Identify build-time tools needed
```
3. **Categorize required packages:**
**Build-Time Tools (must be in root devDependencies):**
- `tsx` - TypeScript execution for Node.js
- `typescript` - TypeScript compiler
- `@types/node` - Node.js type definitions
- `@types/*` - Any TypeScript type packages
- Build tools: `vite`, `webpack`, `rollup`, `esbuild`
- Linters: `eslint`, `prettier`
- Testing: `jest`, `vitest`, `playwright`
**Runtime Dependencies (must be in root dependencies):**
- Framework packages: `next`, `react`, `express`, `vue`
- Runtime utilities needed by any component
- Shared packages used across components
4. **Propose root package.json updates to user:**
```
I detected this is a monorepo with 2 services. App Platform only installs
dependencies from the root package.json.
I need to add these build-time tools to root package.json:
- tsx (for API TypeScript execution)
- typescript (for type checking)
- @types/node (for Node types)
Would you like me to:
A) Add these automatically and proceed
B) Skip this and let build fail (then fix iteratively)
C) Review the full dependency list first
```
5. **If user approves, update root package.json:**
```bash
# Merge component dependencies into root package.json
# Keep existing root dependencies
# Add missing build tools to devDependencies
# Add missing runtime deps to dependencies
```
**Example Transformation:**
**Before (will fail on App Platform):**
```json
// Root package.json
{
"name": "my-monorepo",
"private": true,
"devDependencies": {
"turbo": "^2.5.5"
}
}
// apps/api/package.json
{
"name": "api",
"dependencies": {
"express": "^4.18.2"
},
"devDependencies": {
"tsx": "^4.20.6",
"typescript": "^5.0.0"
}
}
```
**After (will work on App Platform):**
```json
// Root package.json
{
"name": "my-monorepo",
"private": true,
"dependencies": {
"express": "^4.18.2" // Added from api
},
"devDependencies": {
"turbo": "^2.5.5",
"tsx": "^4.20.6", // Added from api
"typescript": "^5.0.0", // Added from api
"@types/node": "^20.0.0" // Added from api
}
}
```
---
#### 1.5.2: Iterative Dependency Fixing (Fallback Approach)
If you skip proactive analysis or it misses something:
1. **Let the build attempt**
2. **Parse build error logs for missing dependencies:**
Common error patterns:
```
/bin/sh: tsx: not found
/bin/sh: tsc: not found
Error: Cannot find module 'typescript'
```
3. **Suggest fix to user:**
```
Build failed because 'tsx' is not found.
This is a monorepo and App Platform only installs dependencies from
root package.json.
Fix: Add 'tsx' to root package.json devDependencies
Would you like me to add it and redeploy?
```
4. **Update root package.json and retry:**
- Add missing package to appropriate section
- Commit and push changes
- Redeploy (automatic if `deploy_on_push: true`)
5. **Repeat up to 3 times** for different missing packages
---
#### 1.5.3: Trade-offs and Considerations
**Approach A: Consolidate All Dependencies (Proactive)**
**Pros:**
- ā
Works on first deployment
- ā
No iteration needed
- ā
Predictable behavior
- ā
User stays in control (approves changes)
**Cons:**
- ā All components get ALL dependencies
- ā Larger `node_modules` in each component
- ā Slower installs
- ā Slightly larger deployment artifacts
**When to use:**
- Small monorepos (2-4 services)
- "Just make it work" scenarios
- User wants fast deployment
**Approach B: Iterative Fixing (Reactive)**
**Pros:**
- ā
Only adds truly needed packages
- ā
Cleaner dependency tree
- ā
Educational for user
**Cons:**
- ā Requires multiple deploy attempts (2-5 minutes each)
- ā More complex error detection
- ā Could miss cascading errors
**When to use:**
- Large monorepos with many services
- Production apps where bundle size matters
- User wants optimal configuration
**Approach C: Hybrid (Recommended)**
1. Proactively analyze and propose changes
2. User reviews and approves
3. If build still fails, fall back to iterative fixing
4. Usually works first time, graceful degradation
---
#### 1.5.4: Special Cases
**Case 1: Using pnpm workspaces**
If `pnpm-workspace.yaml` exists:
```yaml
packages:
- 'apps/*'
- 'packages/*'
```
App Platform buildpack automatically:
- Detects pnpm from `pnpm-lock.yaml`
- Runs `pnpm install` at root
- Respects workspace configuration
**Still need**: Build tools in root package.json
**Case 2: Using npm/yarn workspaces**
If root `package.json` has:
```json
{
"workspaces": ["apps/*", "packages/*"]
}
```
App Platform buildpack automatically:
- Detects workspaces
- Runs `npm install` or `yarn install` at root
- Links workspace packages
**Still need**: Build tools in root package.json
**Case 3: Components use different runtimes**
If dashboard uses `pnpm build` but API uses `tsx src/index.ts`:
- Both commands must have their dependencies in root
- `pnpm` must be in root (detected from lockfile)
- `tsx` must be in root devDependencies
---
#### 1.5.5: Implementation Example (AI Assistant Instructions)
**When detecting a monorepo:**
```typescript
// Pseudo-code for AI assistant
function analyzeMonorepo(repoPath) {
// 1. Detect monorepo structure
const isMonorepo = hasWorkspaceConfig(repoPath);
if (!isMonorepo) return null;
// 2. Find all component package.json files
const components = findComponents(repoPath);
// 3. Extract dependencies from each component
const allDeps = {
dependencies: new Set(),
devDependencies: new Set()
};
for (const component of components) {
const pkg = readPackageJson(component.path);
// Merge runtime dependencies
Object.keys(pkg.dependencies || {}).forEach(dep =>
allDeps.dependencies.add({ name: dep, version: pkg.dependencies[dep] })
);
// Merge build-time dependencies
Object.keys(pkg.devDependencies || {}).forEach(dep =>
allDeps.devDependencies.add({ name: dep, version: pkg.devDependencies[dep] })
);
}
// 4. Check what's missing from root
const rootPkg = readPackageJson(`${repoPath}/package.json`);
const missing = {
dependencies: [],
devDependencies: []
};
allDeps.dependencies.forEach(dep => {
if (!rootPkg.dependencies?.[dep.name]) {
missing.dependencies.push(dep);
}
});
allDeps.devDependencies.forEach(dep => {
if (!rootPkg.devDependencies?.[dep.name]) {
missing.devDependencies.push(dep);
}
});
// 5. Return analysis
return {
isMonorepo: true,
components: components.length,
missing,
recommendation: missing.dependencies.length + missing.devDependencies.length > 0
? "CONSOLIDATE_DEPENDENCIES"
: "NO_ACTION_NEEDED"
};
}
// Then ask user
if (analysis.recommendation === "CONSOLIDATE_DEPENDENCIES") {
askUser(`
Detected monorepo with ${analysis.components} components.
App Platform only installs dependencies from root package.json.
Missing from root:
${formatDependencies(analysis.missing)}
Add these to root package.json?
A) Yes, add automatically
B) No, I'll handle it manually
C) Show me the full proposed package.json
`);
}
```
---
#### 1.5.6: Troubleshooting Monorepo Dependency Issues
**Symptom:** Build fails with "command not found: tsx"
**Diagnosis:**
```bash
# Check build logs
doctl apps logs <app-id> --type build
# Look for:
/bin/sh: tsx: not found
/bin/sh: 1: tsx: not found
```
**Root Cause:** `tsx` is in component `package.json` but not in root
**Fix:**
```bash
# Add to root package.json devDependencies
{
"devDependencies": {
"tsx": "^4.20.6"
}
}
# Commit and push
git add package.json
git commit -m "Add tsx to root for App Platform build"
git push
```
**Symptom:** Build succeeds but run fails with "Cannot find module 'express'"
**Diagnosis:**
```bash
# Check runtime logs
doctl apps logs <app-id> --type run
# Look for:
Error: Cannot find module 'express'
```
**Root Cause:** `express` is in component `package.json` but not in root
**Fix:**
```bash
# Add to root package.json dependencies (NOT devDependencies)
{
"dependencies": {
"express": "^4.18.2"
}
}
```
---
#### 1.5.7: Validation Checklist
Before proceeding to deployment:
- [ ] Identified if repository is a monorepo
- [ ] If monorepo: Analyzed all component package.json files
- [ ] Identified build-time tools needed (tsx, typescript, etc.)
- [ ] Identified runtime dependencies needed
- [ ] Updated root package.json with consolidated dependencies
- [ ] User approved dependency consolidation
- [ ] Committed changes to git
- [ ] Ready to proceed to app spec creation
---
### Step 2: App Migration
Follow these steps to migrate the app:
#### 2.1 Project settings and Build Configuration
**Inspect the repository to understand project structure and let App Platform buildpack auto-detect build/run commands**
1. **Fetch and inspect package.json from the repository:**
- If monorepo or source_dir is specified: look at `[source_dir]/package.json`. Deploy them as separate components per app.
- If root level app: look at root `package.json`
- Identify the framework from dependencies (Next.js, React, Vue, Express, etc.)
- Note if there's a "build" script and "start" script in the scripts section
- Check the "engines" field for Node.js version requirements
2. **Check Vercel configuration for custom settings:**
- Look in `vercel.json` for `buildCommand`, `outputDirectory`, or `framework` settings
- Check Vercel project settings for any custom build/install commands
- Note the framework detected by Vercel
- If Vercel uses custom commands, consider using them; otherwise leave empty for buildpack auto-detection
3. **Validation before migration:**
**Project Structure:**
- Verify package.json exists at the correct location (root or source_dir)
- Check for package-lock.json or yarn.lock (for dependency consistency)
- For monorepos: verify source_dir path is correct relative to repository root
- Supported Node.js version in "engines" field in App Platform, (from https://docs.digitalocean.com/products/app-platform/reference/buildpacks/nodejs/#current-buildpack-version-and-supported-runtimes)
**Build/Run Commands - Prefer buildpack auto-detection:**
- **Default approach:** Leave build and run commands empty to let App Platform buildpack auto-detect based on framework
- **Only specify custom commands if:**
- Vercel uses non-standard build commands (check vercel.json)
- The project has custom scripts that differ from framework defaults
- You need to override buildpack defaults for specific reasons
- **Reference:** [App Platform Node.js buildpack documentation](https://docs.digitalocean.com/products/app-platform/reference/buildpacks/nodejs/) for auto-detection behavior
**For Static Sites:**
- Buildpack will auto-detect framework and build command
- Specify `output_dir` only if different from framework default (e.g., Next.js uses `out` for static export, not `.next`)
- Common output directories: `build`, `dist`, `out`, `public`, `_site`
**For Web Services:**
- Buildpack will auto-detect framework and start command
- Ensure app listens on `process.env.PORT` (App Platform injects this variable)
- Leave run command empty unless using a custom start script
**Install Command:**
- Buildpack auto-detects npm, yarn, or pnpm based on lockfile
- Only specify custom install command if needed (e.g., monorepo workspaces)
**If you need to specify commands:**
- Verify the script exists in package.json "scripts" section
- Or verify the framework package is in dependencies/devDependencies
- Ask the user to confirm if uncertain
#### 2.2 Environment Variables
1. **Collect environment variables:**
**If migrating from Vercel:**
- Read the `.env.<environment>` file from the migration folder (Step 1.1)
- Parse all variables from this file
- If no environment variables were pulled, proceed with manual collection
**If deploying source code directly (no Vercel):**
- Check for `.env.example` or `.env.sample` file in project
- Read `.env.example` to understand required variables
- Ask user for values for each required variable
- Generate secure values where needed (e.g., `AUTH_SECRET`)
2. **Transform system variables:**
- Note: `VERCEL_URL`, `VERCEL_ENV`, any `VERCEL_GIT_*` and Vercel-specific system variables will NOT be available in App Platform
- Warn the user if the app depends on these variables
- Replace `VERCEL_URL` with App Platform URL (set after deployment)
- Replace `VERCEL_ENV` with `NODE_ENV=production`
3. **Prepare environment variables for App Platform:**
- Keep the same variable names (except for Vercel-specific ones)
- Mark sensitive variables (API keys, secrets, database passwords) as `type: SECRET` in app spec
- Generate secure random values where needed:
- `AUTH_SECRET`: `openssl rand -base64 32`
- `STRIPE_WEBHOOK_SECRET`: User provides or placeholder
- If unable to retrieve values, ask user or leave blank (app may have defaults)
4. **Database connection string:**
- If creating managed database in App Platform, use `${db.DATABASE_URL}` reference
- If using external database, ask user for connection string
#### 2.3 Component Type Detection
Determine the appropriate App Platform component type based on the app's characteristics:
**Static Sites:**
- Pure static HTML/CSS/JS with no server-side logic
- Static site generators (Gatsby, Hugo, Jekyll, etc.) that build to an output directory
- React/Vue/Angular SPAs that are fully client-side rendered
- **Create as Static Site component**
- **Configuration needed:**
- `build_command`: Leave empty for buildpack auto-detection (or specify if custom)
- `output_dir`: Only if different from framework default (e.g., `build`, `dist`, `out`, `public`)
**Web Services (Dynamic Sites):**
- Next.js apps with API routes (files in `/pages/api/*` or `/app/api/*`)
- Next.js apps using server-side rendering (SSR) or server components
- Node.js/Express backend applications
- Any app that needs a running server process
- Apps with serverless functions in `/api/*` directory
- **Create as Web Service component**
- **Configuration needed:**
- `build_command`: Leave empty for buildpack auto-detection (or specify if custom)
- `run_command`: Leave empty for buildpack auto-detection (or specify if custom)
- No `output_dir` - the service runs continuously
**How to determine:**
1. Check if app has `/api/*` routes or serverless functions ā **Web Service**
2. Check if Next.js uses `getServerSideProps`, `getInitialProps`, or server components ā **Web Service**
3. Check if there's a server file (like `server.js`, `index.js` with Express) ā **Web Service**
4. If purely static output with no server logic ā **Static Site**
5. When in doubt, check the Vercel deployment type or framework detection
**Multiple Components (Advanced):**
- If the app has both static content AND API routes, you could separate into:
- Option A: Single web service (simplest - handles both static and API)
- Option B: Separate static site + web service components (more complex setup)
- **Recommend Option A** unless user specifically wants separation
- **See section 2.3.5 below for detailed multi-service architecture guidance**
#### 2.3.5 Multi-Service and Monorepo Applications
**App Platform supports deploying multiple components in a single app.** This is different from deploying multiple separate apps.
**One App with Multiple Components vs Multiple Apps:**
| Use One App (Multiple Components) | Use Multiple Apps |
|-----------------------------------|-------------------|
| Services in same repository | Services in different repositories |
| Deploy together on git push | Deploy independently |
| Shared database connection | Separate databases |
| Internal communication (fast, no public internet) | Public API communication |
| Examples: monorepo, microservices, frontend+API+worker | Examples: different products, different teams |
**Component Types in One App:**
- **Multiple web services** (e.g., frontend, API, admin panel)
- **Static sites + web services** (e.g., marketing site + API)
- **Workers** (background jobs, queue processors, continuous processes)
- **Jobs** (one-time tasks, pre-deploy hooks, post-deploy hooks)
- **Databases** (PostgreSQL, ValKey/Redis)
---
**Example 1: Monorepo with Frontend + API + Shared Database**
This is the most common multi-service scenario.
**Repository structure:**
```
monorepo/
āāā apps/
ā āāā web/ # Next.js frontend
ā ā āāā package.json
ā ā āāā src/
ā āāā api/ # Express API
ā āāā package.json
ā āāā src/
āāā packages/
ā āāā shared/ # Shared code
ā āāā package.json
āāā package.json # Root workspace config
āāā pnpm-workspace.yaml # or package-lock.json, yarn.lock
```
**app-spec.yaml:**
```yaml
name: my-fullstack-app
region: nyc1
services:
# Frontend service
- name: web
github:
repo: owner/monorepo
branch: main
deploy_on_push: true
source_dir: /apps/web
build_command: "" # Auto-detect Next.js
run_command: "" # Auto-detect Next.js start
http_port: 8080
instance_count: 1
instance_size_slug: basic-xxs
routes:
- path: / # Frontend handles root and all non-API routes
envs:
- key: API_URL
value: ${api.PRIVATE_URL} # Internal URL to API service (fast, no public internet)
- key: DATABASE_URL
value: ${db.DATABASE_URL}
scope: RUN_AND_BUILD_TIME
- key: NODE_ENV
value: production
# API service
- name: api
github:
repo: owner/monorepo
branch: main
deploy_on_push: true
source_dir: /apps/api
build_command: "" # Auto-detect or specify if custom
run_command: "" # Auto-detect Express/Node
http_port: 8080
instance_count: 1
instance_size_slug: basic-xxs
routes:
- path: /api # API handles all /api/* routes
envs:
- key: DATABASE_URL
value: ${db.DATABASE_URL}
scope: RUN_AND_BUILD_TIME
- key: NODE_ENV
value: production
databases:
- name: db
engine: PG
version: "18"
production: false # Set to true for production tier with backups
```
**Key Points:**
1. Both services reference same repository, different `source_dir`
2. Services can reference each other with `${service-name.PRIVATE_URL}` (internal communication, fast)
3. Public routes are configured per service (`/` for web, `/api` for API)
4. Database is shared via `${db.DATABASE_URL}` reference
5. All services deploy together when code is pushed
6. Buildpack auto-detects framework in each service's directory
**Monorepo Package Manager Detection:**
- If `pnpm-workspace.yaml` exists ā buildpack uses pnpm
- If root `package.json` has "workspaces" field ā buildpack uses npm/yarn workspaces
- Buildpack automatically installs dependencies for each service
---
**Example 2: Adding Workers and Jobs for Background Tasks**
**Repository structure:**
```
app/
āāā web/ # Frontend
āāā api/ # API
āāā worker/ # Background worker (continuous)
ā āāā worker.js # Processes queue, runs continuously
āāā jobs/
āāā migrations/ # Database migrations (one-time)
āāā migrate.js
```
**app-spec.yaml additions:**
```yaml
name: my-app-with-workers
region: nyc1
services:
# ... web and api services as above ...
workers:
# Background worker - runs continuously
- name: email-worker
github:
repo: owner/repo
branch: main
source_dir: /worker
build_command: ""
run_command: "node worker.js" # Continuous process
instance_count: 1
instance_size_slug: basic-xxs
envs:
- key: DATABASE_URL
value: ${db.DATABASE_URL}
- key: REDIS_URL
value: ${cache.DATABASE_URL}
- key: NODE_ENV
value: production
jobs:
# Pre-deploy job - runs before deployment
- name: db-migrate
kind: PRE_DEPLOY
github:
repo: owner/repo
branch: main
source_dir: /jobs/migrations
build_command: ""
run_command: "node migrate.js" # Runs once before deployment
instance_size_slug: basic-xxs
envs:
- key: DATABASE_URL
value: ${db.DATABASE_URL}
databases:
- name: db
engine: PG
version: "18"
- name: cache
engine: REDIS # ValKey (Redis fork)
version: "7"
```
**Worker vs Job:**
- **Worker**: Continuous process (e.g., queue processor, scheduled task runner)
- **Job**: Runs once per deployment (e.g., database migrations, cache warming)
- **Job kinds**:
- `PRE_DEPLOY`: Runs before new code is deployed (migrations)
- `POST_DEPLOY`: Runs after deployment succeeds (cache warming)
---
**Example 3: Separating Static Site from API Service**
Sometimes you want a separate static frontend (SPA) and API backend.
**app-spec.yaml:**
```yaml
name: spa-with-api
region: nyc1
static_sites:
# Static React/Vue/Angular SPA
- name: web
github:
repo: owner/repo
branch: main
source_dir: /frontend
build_command: "npm run build" # Or leave empty for auto-detect
output_dir: dist # Or build, out, public
routes:
- path: /
envs:
- key: VITE_API_URL # Or REACT_APP_API_URL, NEXT_PUBLIC_API_URL
value: ${api.PUBLIC_URL} # Public URL to API
services:
# API service
- name: api
github:
repo: owner/repo
branch: main
source_dir: /backend
build_command: ""
run_command: ""
http_port: 8080
routes:
- path: /api
envs:
- key: DATABASE_URL
value: ${db.DATABASE_URL}
databases:
- name: db
engine: PG
version: "18"
```
**When to use this pattern:**
- SPA that's 100% client-side rendered
- Clear separation between frontend and backend
- Frontend only needs static file hosting
---
**When to Use Multiple Apps vs One App with Multiple Components:**
**Use ONE app with multiple components when:**
- ā
All services are in the same repository (monorepo)
- ā
Services share the same database
- ā
Services need to deploy together (atomic deployments)
- ā
Services communicate internally (low latency requirements)
- ā
Simpler management (one dashboard, one app spec)
**Use MULTIPLE separate apps when:**
- ā
Services are in different repositories
- ā
Services need to deploy independently
- ā
Services have different teams/ownership
- ā
Services need independent scaling and resource management
- ā
Services are truly separate products/systems
---
**Deployment Behavior with Multiple Components:**
- Git push triggers deployment of **ALL components** in the app
- All components must build successfully before deployment
- If **one component fails**, entire deployment fails (rollback to previous)
- Components start simultaneously after successful build
- Database migrations (PRE_DEPLOY jobs) run before services start
**Troubleshooting Multi-Component Deployments:**
If one component fails:
1. Check which component failed in logs: `doctl apps logs <app-id> --type build`
2. Fix the failing component's code or configuration
3. Push fix - all components rebuild
4. Consider deploying components in separate apps if you need independent deployments
**Internal Service Communication:**
- Use `${service-name.PRIVATE_URL}` for fast internal communication (no public internet)
- Example: Frontend calls `${api.PRIVATE_URL}/users` instead of public URL
- Faster and more secure than public URLs
- Private URLs are only accessible within the app's components
**Assistant Instructions:**
- When detecting a monorepo, ask user: "I see this is a monorepo with multiple services. Would you like to deploy them as separate components in one app, or as a single service?"
- Show the user which services you detected (by finding multiple package.json files)
- Propose an app spec with multiple components
- Explain that all services will deploy together
- Confirm the routing strategy (which paths go to which services)
#### 2.4 Feature Translation
**Serverless Functions ā Web Services or Functions:**
- Vercel serverless functions in `/api/*` directory
- Migrate to App Platform web service with API endpoints
- OR guide user to use App Platform Functions
- Note: Edge Functions are NOT directly supported - must refactor to web services
**Cron Jobs:**
- If Vercel has cron jobs invoking API endpoints
- Create native App Platform cron jobs
- Map cron expressions directly
- Update to use App Platform internal URLs instead of `/api/*` HTTP endpoints
**Middleware:**
- Vercel `middleware.ts` is NOT supported in App Platform
- Inform user they need to implement application-level middleware
- This requires code changes in the application itself
**ISR (Incremental Static Regeneration):**
- NOT directly supported in App Platform
- Inform user they have these options:
- Rebuild static site periodically using cron jobs
- Convert to server-side rendering (SSR)
- Use Spaces with cache persistence for future NFS support
**Image Optimization:**
- NOT natively supported in App Platform
- Recommend these alternatives:
1. Cloudflare Images API (fully managed, paid after 5000 transformations)
2. Next.js `next/image` with Sharp in the same container
3. Dedicated worker or cron job with Sharp/libvips processing to Spaces
4. External image CDN service
**Headers and Redirects:**
- Vercel `headers` in `vercel.json` ā Configure CORS in app spec or application middleware
- Vercel `redirects` in `vercel.json` ā App Platform routing rules in app spec
- Vercel `rewrites` in `vercel.json` ā Internal routing configuration in app spec
**Edge Caching:**
- Vercel has configurable multi-tier caching
- App Platform has standard CDN caching (cannot disable or configure extensively)
- Guide user to adjust cache headers in their application
#### 2.5 Create App Platform App
**Using doctl CLI (Primary Method):**
1. **Create app spec YAML file** with configuration based on component type:
**IMPORTANT: Only proceed with app creation after completing validation in step 2.1 and local testing in Step 0**
**For Static Sites:**
```yaml
name: <app-name>
region: <region> # e.g., nyc1, sfo1, ams3
services:
- name: web
github:
repo: <owner>/<repo-name>
branch: main # or from Vercel if migrating
deploy_on_push: true
source_dir: / # or path for monorepo
build_command: "" # empty for buildpack auto-detection
output_dir: "" # only if non-standard
instance_count: 1
instance_size_slug: basic-xxs
envs:
# Add environment variables here
```
**For Web Services (Dynamic Sites):**
```yaml
name: <app-name>
region: <region> # e.g., nyc1, sfo1, ams3
services:
- name: web
github:
repo: <owner>/<repo-name>
branch: main # or from Vercel if migrating
deploy_on_push: true
source_dir: / # or path for monorepo
build_command: "" # empty for buildpack auto-detection
run_command: "" # empty for buildpack auto-detection
http_port: 8080
instance_count: 1
instance_size_slug: basic-xxs
envs:
- key: NODE_ENV
value: production
scope: RUN_AND_BUILD_TIME
# Add other environment variables here
databases:
- name: db
engine: PG
version: "18"
production: false
```
2. **Save the app spec to local filesystem:**
- Save as `app-spec.yaml` in project root or migration folder
- This allows the user to manually create the app if needed
3. **Create app using doctl CLI:**
- Run: `doctl apps create --spec app-spec.yaml`
- Capture app ID from output
- Monitor deployment: `doctl apps get <app-id>`
4. **Set up any required databases:**
- If app uses Vercel Postgres ā Create Managed PostgreSQL
- If app uses Vercel KV ā Create Managed ValKey (Redis fork)
- Update connection strings in environment variables
**Database Configuration Details:**
**Adding Managed Databases in app spec:**
```yaml
databases:
# PostgreSQL database
- name: db
engine: PG
version: "18" # Or "17", "16", "15", "14"
production: false # false = dev tier ($15/mo), true = production tier ($25/mo+)
cluster_name: my-db-cluster # optional - creates new cluster or references existing
# Redis/ValKey cache
- name: cache
engine: REDIS # ValKey (Redis fork, Redis 7.2 compatible)
version: "7"
production: false
```
**Referencing databases in services:**
```yaml
services:
- name: web
# ... other config ...
envs:
# Database URL is injected automatically
- key: DATABASE_URL
value: ${db.DATABASE_URL} # Format: postgresql://user:pass@host:port/dbname?sslmode=require
scope: RUN_AND_BUILD_TIME # Available during build and runtime
# Redis URL
- key: REDIS_URL
value: ${cache.DATABASE_URL} # Format: redis://user:pass@host:port
scope: RUN_TIME # Only available at runtime
```
**Connection string formats:**
- **PostgreSQL**: `postgresql://username:password@host:port/database?sslmode=require`
- **Redis/ValKey**: `redis://username:password@host:port`
- SSL/TLS is enforced for all connections to managed databases and cannot be disabled
- Credentials are automatically generated and rotated
**Database tiers:**
- **Development** (`production: false`): $15/month, 1 GB RAM, 10 GB disk, 1 node, daily backups
- **Production** (`production: true`): Starting at $25/month, 2 GB RAM, 25 GB disk, automatic failover, point-in-time recovery
**Using external database (not managed by App Platform):**
If you want to use an existing external database:
```yaml
services:
- name: web
envs:
- key: DATABASE_URL
value: "postgresql://external-host.com:5432/mydb?sslmode=require"
type: SECRET # Mark as secret to encrypt in dashboard
```
**Database migrations:**
Use PRE_DEPLOY jobs to run migrations before deployment:
```yaml
jobs:
- name: db-migrate
kind: PRE_DEPLOY
github:
repo: owner/repo
branch: main
source_dir: /
run_command: "npm run migrate" # or "pnpm migrate", "yarn migrate"
instance_size_slug: basic-xxs
envs:
- key: DATABASE_URL
value: ${db.DATABASE_URL}
```
Common migration commands:
- Prisma: `npx prisma migrate deploy`
- Drizzle: `npm run db:push` or `npm run db:migrate`
- TypeORM: `npm run typeorm migration:run`
- Knex: `npm run knex migrate:latest`
- Sequelize: `npx sequelize-cli db:migrate`
**Important notes:**
- Database creation can take 10-15 minutes on first deployment
- Databases persist across deployments and app deletions (must manually delete)
- Connection pooling is recommended (use connection pool library)
- **Data migration** from Vercel Postgres is a separate manual process (not automated by this guide)
**Data migration strategy (manual process):**
1. Create new App Platform database
2. Use `pg_dump` to export from Vercel Postgres
3. Use `psql` or `pg_restore` to import to App Platform database
4. Test thoroughly before switching DNS
5. Keep Vercel database running during transition
6. Consider using database migration tools like `pg_dump`/`pg_restore`, or managed migration services
#### 2.6 Configure Domains
**Using doctl CLI (Primary Method):**
1. **Add custom domains to the App Platform app:**
- Run: `doctl apps create-domain <app-id> --domain <domain-name>`
- For each domain that was in Vercel (if migrating), add it to App Platform
- App Platform will provide DNS configuration details
- Both apex domains (example.com) and subdomains (www.example.com) are supported
- App Platform will automatically provision SSL certificates
2. **Get DNS configuration:**
- Get default ingress: `doctl apps get <app-id> --format DefaultIngress`
- Get domain configuration: `doctl apps get-domain <app-id> <domain-name>`
3. **Provide DNS update instructions to the user:**
- Display these instructions as post deployment instruction
- List each domain that needs to be updated
- Ask if the user wants to manage domains through DigitalOcean or keep it self managed (outside of DigitalOcean)
- If the user wants to manage domains through DigitalOcean, then display:
```md
## Move DNS records to DigitalOcean
Update your DNS provider with our nameservers.
DigitalOcean Nameservers
- ns1.digitalocean.com
- ns2.digitalocean.com
- ns3.digitalocean.com
```
- If the user wants to keep it self managed:
- Get the default_ingress using `doctl apps get <app-id> --format DefaultIngress`
- Display:
```md
Add a new CNAME record using the provided alias to point your domain to your DigitalOcean app.
## CNAME alias
<default_ingress>
Check if CNAME flattening is supported for root domains. If your provider doesn't support this method, you can create an A record that points to your root domain.
## A Records
- 162.159.140.98
- 172.66.0.96
```
- Note: default ingress is only available after a successful deployment
4. **Important notes for the user:**
- DNS propagation can take up to 48 hours
- Wildcard domains (*.example.com) are also supported if needed
#### 2.8 Assisted Deployment Troubleshooting
**If deployment fails, follow this assisted troubleshooting process:**
The AI assistant will analyze errors and suggest fixes for you to review and approve. This is not fully automatic - you remain in control.
1. **Check deployment status:**
- Use `doctl apps get <app-id>` to get deployment status
- Status will be either: ACTIVE, DEPLOYED (success) or FAILED, ERROR (failure)
2. **Fetch deployment logs immediately:**
- Get build logs: `doctl apps logs <app-id> --type build`
- Get runtime logs: `doctl apps logs <app-id> --type run`
- Get deployment logs: `doctl apps get-deployment <app-id> <deployment-id>`
- Parse the error messages to identify the failure type
3. **Analyze the error and suggest fixes:**
**For "command not found" or "script not found" errors:**
- **Suggested fix - prefer buildpack auto-detection:**
- **First attempt:** Leave build/run commands empty to let buildpack auto-detect
- This works for most standard frameworks (Next.js, React, Vue, etc.)
- If this fails, then try checking package.json for custom scripts:
- Read package.json from the migration folder at `./vercel-migration/<app-name>-migration/package.json`
- If Vercel used a custom script (check vercel.json), use that specific script
- If project has custom scripts that differ from framework defaults, specify those
- Update the app with corrected configuration and redeploy
**For "no start command" or run command errors:**
- **Suggested fix - prefer buildpack auto-detection:**
- **First attempt:** Leave run command empty to let buildpack auto-detect
- Buildpack will use framework defaults or package.json "start" script
- Only specify custom run command if buildpack auto-detection fails
- Update run command and redeploy
**For "output directory not found" errors:**
- **Suggested fix:**
- Check common output directories: `build`, `dist`, `out`, `public`, `.next`
- **Check .gitignore in migration folder** for build output patterns
- Look in build logs for "writing output to..." messages
- Update output_dir and redeploy
**For "module not found" or dependency errors:**
- **Check migration folder for lockfile** (package-lock.json or yarn.lock)
- **Suggested fix options:**
- If lockfile is in a subdirectory, update source_dir
- If using private packages, check if registry auth env vars are needed
- Add NODE_ENV=production if missing
**For port/health check failures (service starts but fails health check):**
- Fetch runtime logs to see what port the app is listening on
- **Suggested fix:**
- Check if app is hardcoded to wrong port
- Ensure PORT environment variable is set
- Update HTTP port configuration if needed
**For monorepo/source directory errors:**
- **Suggested fix:**
- Check Vercel config for rootDirectory
- **Search migration folder** for package.json in subdirectories
- Look for workspace configuration files (lerna.json, nx.json, etc.)
- Update source_dir to correct path and redeploy
- Deploy the mono repo under 1 app, where each repository service is a `service component` or `static site component` depending on the repository service. Ask the user for confirmation.
4. **Apply the fix and redeploy:**
- Update app spec YAML with fixes
- Update app using doctl: `doctl apps update <app-id> --spec app-spec.yaml`
- Or update specific config: `doctl apps update <app-id> --env <key>=<value>`
- Wait for deployment to complete and check status again
5. **Retry logic:**
- If first suggested fix doesn't work, analyze logs again and try alternative approach
- Maximum 3 assisted troubleshooting attempts per app
- After 3 failed attempts, report to user with detailed error analysis and request manual intervention
6. **Report fix applied:**
- Tell the user what error was found
- Tell the user what fix was applied
- Show the updated configuration
### Step 3: Validation and Automatic Recovery
After migrating the app:
1. **Verify deployment succeeded** in App Platform (use `doctl apps get <app-id>` to check status)
- Wait for the deployment to complete (this may take several minutes)
- Check deployment status: should be "ACTIVE" or "DEPLOYED"
2. **If deployment FAILED, begin assisted troubleshooting:**
- Follow the process in section 2.8 "Assisted Deployment Troubleshooting"
- Fetch logs, analyze errors, suggest fixes for user to review
- Retry up to 3 times with different suggested approaches