Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
312 changes: 312 additions & 0 deletions plugins/render/skills/render-migrate-from-heroku/SKILL.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# Blueprint Example: Heroku Migration with Project/Environment Pattern

This example shows a complete `render.yaml` for migrating a typical Heroku app with a web dyno, worker dyno, Heroku Scheduler (clock), Postgres, and Redis. It uses the `projects`/`environments` pattern to group all resources in a single Render project.

References: [Blueprint docs](https://render.com/docs/blueprint-spec#projects-and-environments) | [Blueprint YAML JSON schema](https://render.com/schema/render.yaml.json)

## Full Example

Assumes a Node.js app named `acme-app` migrating from Heroku US region.

```yaml
previews:
generation: off
projects:
- name: acme-app
environments:
- name: production
services:
# Web service (from Heroku web dyno)
- type: web
name: acme-app-web
runtime: node
plan: starter
region: oregon
buildCommand: npm ci && npm run build
startCommand: npm start
healthCheckPath: /health
envVars:
- key: NODE_ENV
value: production
- key: DATABASE_URL
fromDatabase:
name: acme-app-db
property: connectionString
- key: REDIS_URL
fromService:
type: keyvalue
name: acme-app-cache
property: connectionString
- key: APP_NAME
value: acme-app
- key: LOG_LEVEL
value: info
- key: STRIPE_API_KEY
sync: false
- key: JWT_SECRET
sync: false

# Background worker (from Heroku worker dyno)
- type: worker
name: acme-app-worker
runtime: node
plan: starter
region: oregon
buildCommand: npm ci
startCommand: node worker.js
envVars:
- key: DATABASE_URL
fromDatabase:
name: acme-app-db
property: connectionString
- key: REDIS_URL
fromService:
type: keyvalue
name: acme-app-cache
property: connectionString
- key: APP_NAME
value: acme-app
- key: LOG_LEVEL
value: info
- key: STRIPE_API_KEY
sync: false

# Cron job (from Heroku Scheduler or clock dyno)
- type: cron
name: acme-app-cron
runtime: node
plan: starter
region: oregon
schedule: "0 * * * *"
buildCommand: npm ci
startCommand: node scripts/scheduled-task.js
envVars:
- key: DATABASE_URL
fromDatabase:
name: acme-app-db
property: connectionString
- key: APP_NAME
value: acme-app
- key: STRIPE_API_KEY
sync: false

# Key Value (from Heroku Data for Redis)
- type: keyvalue
name: acme-app-cache
plan: starter
ipAllowList:
- source: 0.0.0.0/0
description: everywhere

databases:
# Postgres (from Heroku Postgres)
- name: acme-app-db
plan: basic-1gb
diskSizeGB: 10 # carried over from Heroku plan allocation

```

## Key Patterns

### Service references

Use `fromDatabase` and `fromService` instead of hardcoding connection strings:

```yaml
# Postgres connection string
- key: DATABASE_URL
fromDatabase:
name: acme-app-db
property: connectionString

# Key Value (Redis) connection string
- key: REDIS_URL
fromService:
type: keyvalue
name: acme-app-cache
property: connectionString
```

### Environment variables

Define env vars directly on each service. Do not use `envVarGroups` — they can cause misapplication issues during Blueprint sync.

### Secrets

Mark secrets with `sync: false` so the user is prompted in the Dashboard:

```yaml
- key: STRIPE_API_KEY
sync: false
```

Render prompts for these values only during the initial Blueprint apply. For updates after initial creation, set secrets manually in the Dashboard or via MCP `update_environment_variables`.

## Blueprint Rules

Follow these rules when generating a `render.yaml` for migration:

- **Always use the `projects:`/`environments:` pattern** — the YAML must start with a `projects:` key. Never use flat top-level `services:` or `databases:` keys.
- **Set every `plan:` field** using the [service mapping](service-mapping.md) — look up the Heroku dyno size or add-on plan and use the mapped Render plan. Never hardcode `starter` without checking the mapping first.
- **Set `diskSizeGB` on databases** — carry over the Heroku disk allocation from the [service mapping](service-mapping.md). Round up to 1 or the nearest multiple of 5. Render storage is expandable and can be resized later.
- Always include `previews: { generation: off }` at the root level to disable preview environments by default.
- Use `fromDatabase` for `DATABASE_URL` — never hardcode connection strings.
- Use `fromService` with `type: keyvalue` and `property: connectionString` for `REDIS_URL`.
- Define env vars directly on each service (do not use `envVarGroups`).
- Mark secrets with `sync: false` (user fills these in the Dashboard during Blueprint apply).
- Map region from Heroku using the [service mapping](service-mapping.md).
- Only include service/database blocks that the Heroku app actually uses.

## Plan Selection

Set every `plan:` field by looking up the Heroku dyno size or add-on plan in the [service mapping](service-mapping.md). The example above uses `starter` and `basic-1gb` as representative values. In a real migration, replace these with the mapped plan for the actual Heroku configuration.

**Fallback defaults** (when the Heroku plan is unknown):

| Service type | Fallback plan |
|---|---|
| Web / worker / cron / static / pserv | `starter` |
| Key Value | `starter` |
| Postgres | `basic-1gb` |

## Adapting This Example

- **Python app:** Change `runtime: node` to `runtime: python`, update build/start commands (e.g., `pip install -r requirements.txt`, `gunicorn app:app`)
- **Ruby app:** Change to `runtime: ruby`, update commands (e.g., `bundle install`, `bundle exec puma`)
- **No worker:** Remove the `type: worker` service block
- **No cron:** Remove the `type: cron` service block
- **No Redis:** Remove the `type: keyvalue` service block and any `REDIS_URL` env var references
- **No Postgres:** Remove the `databases` section and any `DATABASE_URL` env var references
- **Static site:** Replace `type: web` with `runtime: static` and add `staticPublishPath`
- **EU region:** Change `region: oregon` to `region: frankfurt` (maps from Heroku `eu` region)
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Buildpack, Procfile, and Build Command Mapping

## Buildpack → Render Runtime + Build Command

Render needs explicit `buildCommand` and `startCommand`. Determine the runtime from local project files (dependency files, Procfile), or from Heroku buildpack URLs if available via MCP or `app.json`. Use these defaults, then refine based on the app's actual Procfile and package config.

### Node.js

| Buildpack | `heroku/nodejs` or `heroku-community/nodejs` |
|-----------|----------------------------------------------|
| Render runtime | `node` |
| Default build command | `npm install && npm run build` |
| Fallback build (no build script) | `npm install` |
| Detection | Check `package.json` for `build` script. If missing, use fallback. |

**Common variations:**
- Yarn: `yarn install && yarn build` (detect via `yarn.lock` presence)
- pnpm: `pnpm install && pnpm run build` (detect via `pnpm-lock.yaml`)
- TypeScript without build script: `npm install && npx tsc`
- Next.js: `npm install && npm run build` (start: `npm start` or `next start`)
- Vite/CRA (static): use `create_static_site` instead, publishPath `dist` or `build`

### Python

| Buildpack | `heroku/python` |
|-----------|-----------------|
| Render runtime | `python` |
| Default build command | `pip install -r requirements.txt` |
| Django build | `pip install -r requirements.txt && python manage.py collectstatic --noinput` |
| Detection | Check for `requirements.txt`, `Pipfile`, or `pyproject.toml` |

**Common variations:**
- Pipenv: `pip install pipenv && pipenv install`
- Poetry: `pip install poetry && poetry install`
- Django: look for `collectstatic` in Procfile or `django` in requirements
- FastAPI/Flask: straightforward `pip install -r requirements.txt`

### Ruby

| Buildpack | `heroku/ruby` |
|-----------|---------------|
| Render runtime | `ruby` |
| Default build command | `bundle install` |
| Rails build | `bundle install && bundle exec rake assets:precompile` |
| Detection | Check for `Gemfile`. If `rails` gem present, use Rails build. |

### Go

| Buildpack | `heroku/go` |
|-----------|-------------|
| Render runtime | `go` |
| Default build command | `go build -o app .` |
| Detection | Check for `go.mod` |

### Rust

| Buildpack | `emk/rust` or custom |
|-----------|----------------------|
| Render runtime | `rust` |
| Default build command | `cargo build --release` |
| Start command | `./target/release/<binary-name>` |

### Java / Scala / PHP / Multi-buildpack

| Buildpack | Any not listed above |
|-----------|----------------------|
| Render runtime | `docker` |
| Action | Tell user they need a Dockerfile. Offer to generate one. |

## Procfile Parsing

Heroku Procfiles define process types. Extract these to map to Render services.

### Format
```
<process-type>: <command>
```

### Mapping Rules

| Procfile entry | Render service type | Render MCP tool | `startCommand` |
|---------------|--------------------|-----------------|-----------------|
| `web: <cmd>` | Web Service | `create_web_service` | `<cmd>` |
| `worker: <cmd>` | Background Worker | Blueprint `type: worker` | `<cmd>` (MCP cannot create workers) |
| `clock: <cmd>` | Cron Job | `create_cron_job` | `<cmd>` (ask user for schedule) |
| `release: <cmd>` | Pre-deploy command | N/A | Add as build command suffix |

### Common Procfile Patterns

**Node.js:**
```
web: npm start
web: node server.js
web: next start -p $PORT
worker: node worker.js
```

**Python:**
```
web: gunicorn app:app
web: gunicorn myproject.wsgi --log-file -
web: uvicorn main:app --host 0.0.0.0 --port $PORT
worker: celery -A myproject worker
clock: celery -A myproject beat
release: python manage.py migrate
```

**Ruby:**
```
web: bundle exec puma -C config/puma.rb
worker: bundle exec sidekiq
release: bundle exec rake db:migrate
```

### Runtime Version Handling

Do not assume or fabricate specific runtime versions for Render. Instead, carry over the version the Heroku app already uses:

| Heroku file | What it contains | Render equivalent |
|---|---|---|
| `runtime.txt` | `python-3.11.6` | Set `PYTHON_VERSION=3.11.6` env var |
| `runtime.txt` | `ruby-3.2.2` | Render auto-detects from `ruby-3.2.2` in `Gemfile` |
| `.node-version` or `engines` in `package.json` | `18.17.0` | Set `NODE_VERSION=18.17.0` env var |
| `.go-version` or `go.mod` | `1.21` | Render auto-detects from `go.mod` |

**Rules:**
- If the Heroku app pins a version via `runtime.txt` or similar, include the equivalent env var in the Blueprint
- If no version is pinned, do not specify one — Render uses its own defaults
- **Never state what Render's default version is** — it changes over time and any claim may be wrong

### PORT Handling

Heroku sets `$PORT` dynamically. Render also sets `PORT` (default 10000). Most Procfile commands work as-is. If the command hardcodes a port, it needs updating.

- `--port $PORT` → works on both platforms
- `--port 5000` → change to `--port $PORT` or `--port 10000`
- Render auto-detects the port for common frameworks

### Release Phase

Heroku's `release:` process type runs before each deploy. Render has no direct equivalent. Options:
1. Append to build command: `pip install -r requirements.txt && python manage.py migrate`
2. Use Render's pre-deploy command feature (if available for the service type)
3. Flag for the user to handle manually

## Static Site Detection

If the Heroku app is a static site (React, Vue, Gatsby, etc.), use `create_static_site` instead of `create_web_service`.

**Indicators:**
- Buildpack is `heroku-community/static` or `heroku/heroku-buildpack-static`
- Procfile is missing or only has `web: bin/boot` (static buildpack default)
- `package.json` has framework deps: `react-scripts`, `vue`, `gatsby`, `vite`, `@angular/cli`
- `static.json` file present in repo root

**Static site config:**
| Framework | Build command | Publish path |
|-----------|--------------|--------------|
| Create React App | `npm install && npm run build` | `build` |
| Vite (React/Vue) | `npm install && npm run build` | `dist` |
| Gatsby | `npm install && gatsby build` | `public` |
| Next.js (static export) | `npm install && next build` | `out` |
| Angular | `npm install && ng build` | `dist/<project-name>` |
| Hugo | `hugo` | `public` |
Loading