Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Render: From Azure App Service to Render

For .NET engineers who know: Azure App Service, Azure Static Web Apps, Azure Database for PostgreSQL, Azure Cache for Redis, deployment slots You’ll learn: How Render maps to Azure hosting services, where it simplifies your workflow, and how to deploy a Next.js frontend and NestJS API to production Time: 15-20 minutes


The .NET Way (What You Already Know)

Hosting a .NET web app on Azure involves:

  • Azure App Service — managed PaaS for web apps and APIs
  • Azure Static Web Apps — CDN-backed hosting for SPAs
  • Azure Database for PostgreSQL — managed Postgres
  • Azure Cache for Redis — managed Redis
  • Deployment slots — staging environments with swap capability
  • Azure Key Vault — secrets management
  • Application Insights — monitoring and telemetry
  • Virtual Networks — private network isolation
  • App Service Plans — the billing unit (B1, P1v3, etc.)

The Azure experience is powerful and enterprise-grade. It is also complex: you navigate resource groups, service plans, connection strings, managed identities, private endpoints, and a UI that changes frequently.

Render trades some of that power for radical simplicity. For most JS/TS projects, you’ll ship faster on Render and spend less time on infrastructure.


The Render Way

Concept Mapping

Azure ServiceRender EquivalentNotes
App Service (Linux)Web ServiceAuto-deploys from GitHub
Static Web AppsStatic SiteCDN-backed, free tier available
Azure Database for PostgreSQLPostgreSQLManaged, automatic backups
Azure Cache for RedisRedisManaged Redis instance
WebJobs / Azure Functions (timer)Cron JobsScheduled commands
Deployment SlotsPreview EnvironmentsAuto-created per branch/PR
App Service PlanInstance Type (Starter/Standard/Pro)Vertical scaling options
App SettingsEnvironment VariablesPer-service, in dashboard or YAML
Key VaultSecret Files + Env VarsRender encrypts env vars at rest
Application InsightsRender Metrics + LogsBasic metrics, log streaming
Custom Domain + TLSCustom DomainsFree TLS via Let’s Encrypt
Health checksHealth Check PathConfigures restart on failure
Deployment CenterAuto-deploy from GitHubSet repo + branch, done
ARM Templates / Biceprender.yaml (Infrastructure as Code)Optional but recommended

Render Service Types

Web Service — for APIs, full-stack apps, anything with a server process:

  • Runs any Docker image or auto-detects Node.js, Python, Ruby, Go, Rust
  • Persistent process with a public HTTPS URL
  • Auto-restarts on crash
  • Zero-downtime deploys (waits for new instance to be healthy)

Static Site — for SPAs, Next.js static export, documentation:

  • Serves files from a build output directory
  • Global CDN
  • Free tier available
  • Supports custom redirects and rewrites

Background Worker — like a Web Service but with no public port:

  • For queue consumers, scheduled background jobs, etc.

Cron Job — runs a command on a schedule:

  • Cron expression syntax
  • Runs in an isolated container
  • Logs available in dashboard

PostgreSQL — managed PostgreSQL 14/15/16:

  • Automatic daily backups
  • Connection pooling built-in
  • Available from other Render services via internal URL (no public exposure needed)

Redis — managed Redis:

  • Available via internal URL within your Render account
  • Persistence options

Deploying a Web Service

The simplest path: connect your GitHub repo in the Render dashboard, set a build command and start command, and Render handles the rest.

For a NestJS API:

Build Command: npm ci && npm run build
Start Command: node dist/main.js

For a Next.js app (server-side rendering):

Build Command: npm ci && npm run build
Start Command: npm start

Environment Variables: Set them in the Render dashboard under your service’s “Environment” tab, or define them in render.yaml (see below).


render.yaml — Infrastructure as Code

Define your entire stack in a single file at the repo root:

# render.yaml
services:
  # NestJS API
  - type: web
    name: my-api
    runtime: node
    region: oregon
    plan: starter
    buildCommand: pnpm install --frozen-lockfile && pnpm run build
    startCommand: node dist/main.js
    healthCheckPath: /health
    envVars:
      - key: NODE_ENV
        value: production
      - key: PORT
        value: 10000
      - key: DATABASE_URL
        fromDatabase:
          name: my-postgres
          property: connectionString
      - key: REDIS_URL
        fromService:
          name: my-redis
          type: redis
          property: connectionString
      - key: JWT_SECRET
        generateValue: true           # Render generates a random secret
      - key: SENDGRID_API_KEY
        sync: false                   # Tells Render: prompt for this in dashboard

  # Next.js frontend
  - type: web
    name: my-frontend
    runtime: node
    region: oregon
    plan: starter
    buildCommand: pnpm install --frozen-lockfile && pnpm run build
    startCommand: pnpm start
    healthCheckPath: /
    envVars:
      - key: NEXT_PUBLIC_API_URL
        value: https://my-api.onrender.com

  # Background worker
  - type: worker
    name: my-queue-worker
    runtime: node
    buildCommand: pnpm install --frozen-lockfile && pnpm run build
    startCommand: node dist/workers/queue.js
    envVars:
      - key: DATABASE_URL
        fromDatabase:
          name: my-postgres
          property: connectionString

  # Nightly cleanup job
  - type: cron
    name: db-cleanup
    runtime: node
    schedule: "0 3 * * *"            # 3am UTC every day
    buildCommand: pnpm install --frozen-lockfile && pnpm run build
    startCommand: node dist/scripts/cleanup.js
    envVars:
      - key: DATABASE_URL
        fromDatabase:
          name: my-postgres
          property: connectionString

databases:
  - name: my-postgres
    databaseName: myapp
    user: myapp
    plan: starter
    region: oregon

  - name: my-redis
    plan: starter
    region: oregon

Apply this with:

# Install Render CLI
npm install -g @render-oss/render-cli

# Apply (creates or updates services)
render deploy --yes

Or push render.yaml to your repo and Render auto-detects it on first connection.


Auto-Deploy from GitHub

  1. Connect your GitHub account in Render dashboard
  2. Create a new Web Service
  3. Select your repo and branch (main)
  4. Set build and start commands
  5. Every push to main triggers a new deploy automatically

How it works:

  • Render clones your repo at the pushed commit
  • Runs the build command in an isolated build environment
  • If build succeeds, starts the new instance
  • Health check passes → traffic switches to new instance (zero-downtime)
  • If health check fails → old instance keeps running, deploy marked as failed

To disable auto-deploy (manual deploys only):

services:
  - type: web
    name: my-api
    autoDeploy: false

Or trigger deploys via the Render API from your GitHub Actions workflow (covered in 6.2):

curl -X POST \
  -H "Authorization: Bearer $RENDER_API_KEY" \
  "https://api.render.com/v1/services/$SERVICE_ID/deploys" \
  -H "Content-Type: application/json" \
  -d '{}'

Preview Environments

Render’s Preview Environments create a complete copy of your stack for every pull request — equivalent to deployment slots but automatic and branch-scoped.

Configure in render.yaml:

previewsEnabled: true
previewPlan: starter

services:
  - type: web
    name: my-api
    previewsEnabled: true
    # Preview URL will be: my-api-pr-42.onrender.com

When a PR is opened, Render:

  1. Creates a new service instance with the PR branch code
  2. Provisions a temporary database (if configured)
  3. Posts the preview URL to the GitHub PR as a comment
  4. Tears down everything when the PR is merged or closed

This replaces the Azure App Service deployment slots pattern but requires zero manual configuration per PR.


Health Checks

Configure a health check endpoint so Render knows when your service is actually ready:

// NestJS: src/health/health.controller.ts
import { Controller, Get } from '@nestjs/common';

@Controller('health')
export class HealthController {
  @Get()
  check() {
    return { status: 'ok', timestamp: new Date().toISOString() };
  }
}

In render.yaml:

services:
  - type: web
    name: my-api
    healthCheckPath: /health

Render polls this endpoint after deploy. If it returns a non-2xx status within the timeout window, the deploy is rolled back.


Custom Domains and TLS

  1. Add your domain in Render dashboard → Service → Settings → Custom Domains
  2. Add the DNS records shown (CNAME or A record)
  3. TLS certificate is provisioned automatically via Let’s Encrypt
  4. Certificate auto-renews

For apex domains (example.com vs www.example.com), Render supports A records pointing to their load balancer IP. Use www with a CNAME when possible for better reliability.


Scaling

Render’s scaling model is simpler than Azure’s:

Vertical scaling — change instance type:

PlanRAMCPUUse Case
Starter512 MB0.5 CPUDev, low traffic
Standard2 GB1 CPUMost production apps
Pro4 GB2 CPUHigh traffic APIs
Pro Plus8 GB4 CPUMemory-intensive workloads

Horizontal scaling — multiple instances (Standard plan and above):

services:
  - type: web
    name: my-api
    plan: standard
    scaling:
      minInstances: 2
      maxInstances: 10
      targetMemoryPercent: 80       # Scale out when memory > 80%
      targetCPUPercent: 75          # Scale out when CPU > 75%

Note: horizontal scaling requires sticky sessions or stateless design. Your app should not store state in-process (use Redis for session storage).


Monitoring and Logs

Logs:

# Install Render CLI
render logs my-api --tail              # Tail live logs
render logs my-api --since 1h         # Last hour

Or view in the dashboard under your service’s “Logs” tab. Logs stream in real time.

Metrics:

The dashboard shows CPU, memory, and request throughput graphs. For detailed APM, integrate a third-party:

// src/main.ts — Sentry integration
import * as Sentry from '@sentry/node';

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
});

For structured logging that Render captures:

import pino from 'pino';

const logger = pino({
  level: process.env.LOG_LEVEL ?? 'info',
  // Render captures stdout — just log JSON and it's searchable
});

Render vs Azure: Honest Comparison

Where Render wins:

CapabilityRender Advantage
Setup timeMinutes vs hours
PricingPredictable flat rates, no egress fees
Preview environmentsAutomatic, zero config
Developer experienceSimple dashboard, clear logs
Free tierStatic sites and one web service free
Managed TLSAutomatic, no cert management

Where Azure wins:

CapabilityAzure Advantage
Virtual NetworksPrivate network isolation (Render services are publicly reachable)
Multi-regionRender is single-region per service
Enterprise complianceSOC 2 Type II, HIPAA, FedRAMP
ScaleRender tops out at ~20 instances; Azure scales to hundreds
Advanced networkingVNet integration, private endpoints, WAF
Existing Azure ecosystemIf you have Azure AD, CosmosDB, Service Bus

For most product teams with less than 100k daily active users, Render is the better choice. The operational simplicity compounds over time — fewer incidents, faster iteration, lower DevOps cost.


Full Deployment Example: Next.js + NestJS

Project structure:

my-project/
  apps/
    web/          # Next.js
    api/          # NestJS
  render.yaml

render.yaml:

services:
  - type: web
    name: my-project-api
    runtime: node
    region: oregon
    plan: starter
    rootDir: apps/api
    buildCommand: npm ci && npm run build
    startCommand: node dist/main.js
    healthCheckPath: /health
    previewsEnabled: true
    envVars:
      - key: NODE_ENV
        value: production
      - key: PORT
        value: 10000
      - key: DATABASE_URL
        fromDatabase:
          name: my-project-db
          property: connectionString
      - key: JWT_SECRET
        generateValue: true
      - key: CORS_ORIGIN
        value: https://my-project-web.onrender.com

  - type: web
    name: my-project-web
    runtime: node
    region: oregon
    plan: starter
    rootDir: apps/web
    buildCommand: npm ci && npm run build
    startCommand: npm start
    healthCheckPath: /
    previewsEnabled: true
    envVars:
      - key: NEXT_PUBLIC_API_URL
        value: https://my-project-api.onrender.com

databases:
  - name: my-project-db
    databaseName: myproject
    plan: starter
    region: oregon

NestJS health endpoint:

// apps/api/src/app.module.ts
import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { HealthController } from './health/health.controller';

@Module({
  imports: [TerminusModule],
  controllers: [HealthController],
})
export class AppModule {}
// apps/api/src/health/health.controller.ts
import { Controller, Get } from '@nestjs/common';
import { HealthCheck, HealthCheckService, PrismaHealthIndicator } from '@nestjs/terminus';

@Controller('health')
export class HealthController {
  constructor(
    private health: HealthCheckService,
    private db: PrismaHealthIndicator,
  ) {}

  @Get()
  @HealthCheck()
  check() {
    return this.health.check([
      () => this.db.pingCheck('database'),
    ]);
  }
}

Next.js handles the PORT variable automatically, but ensure next.config.js doesn’t hardcode ports:

// apps/web/next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  async rewrites() {
    return [
      {
        source: '/api/:path*',
        destination: `${process.env.NEXT_PUBLIC_API_URL}/:path*`,
      },
    ];
  },
};

module.exports = nextConfig;

Deploy:

# Push render.yaml to your repo
git add render.yaml
git commit -m "chore(infra): add render.yaml"
git push origin main

# Render auto-deploys on push to main
# Check status:
render deploy --wait

Gotchas for .NET Engineers

Gotcha 1: Free and Starter tier services spin down after 15 minutes of inactivity. On the free tier and Starter plan, web services sleep when idle. The first request after sleep incurs a cold start of 30-60 seconds. This is normal for dev/staging. For production, use Standard plan or higher, which keeps instances always running.

Gotcha 2: Render uses ephemeral disk — nothing written to disk persists across deploys. Unlike App Service where you can write files to wwwroot and they persist, Render instances have a read-only filesystem (except /tmp). Store files in S3/R2 object storage, not on disk. Database migrations must be idempotent because they may run against an existing database.

Gotcha 3: Internal URLs are not the same as public URLs. Render services on the same account communicate via internal URLs (e.g., http://my-api:10000), not public HTTPS URLs. Internal communication is free and faster. If your frontend calls the API using the public URL internally, you’re adding unnecessary latency and egress. Set NEXT_PUBLIC_API_URL to the public URL (for browser clients) and a separate server-side API_URL to the internal URL.

Gotcha 4: PostgreSQL connection pool exhaustion. Render’s managed Postgres has a default max connection limit (25 for Starter). Node.js apps often open more connections than .NET apps because of async concurrency. Use a connection pooler. Prisma recommends connection limit settings:

DATABASE_URL=postgresql://user:pass@host:5432/db?connection_limit=5&pool_timeout=10

Or use pgbouncer (Render offers this as an add-on) for high-concurrency workloads.

Gotcha 5: render.yaml applies on the first deploy but not all subsequent changes automatically. Not all render.yaml fields are live-updated on every push. Plan changes, instance count, and some infrastructure changes require applying through the Render dashboard or CLI. Environment variables defined in render.yaml with sync: false must be set manually in the dashboard — they are placeholders, not values.

Gotcha 6: Preview environments share the main database by default. Unless you explicitly configure a separate database for previews, preview environment services will use the same DATABASE_URL as your main service — which means preview PRs can mutate production data. Always scope preview environments to use a separate test database or disable the database in preview configuration.


Hands-On Exercise

Deploy a minimal NestJS API to Render:

# Create a minimal NestJS app
npm install -g @nestjs/cli
nest new render-demo
cd render-demo

# Add a health endpoint
nest generate controller health

# Edit src/health/health.controller.ts
cat > src/health/health.controller.ts << 'EOF'
import { Controller, Get } from '@nestjs/common';

@Controller('health')
export class HealthController {
  @Get()
  check() {
    return {
      status: 'ok',
      timestamp: new Date().toISOString(),
      node: process.version,
    };
  }
}
EOF

# Create render.yaml
cat > render.yaml << 'EOF'
services:
  - type: web
    name: render-demo-api
    runtime: node
    plan: starter
    buildCommand: npm ci && npm run build
    startCommand: node dist/main.js
    healthCheckPath: /health
    envVars:
      - key: NODE_ENV
        value: production
      - key: PORT
        value: 10000
EOF

# Push to GitHub
git add .
git commit -m "feat: add NestJS API with render.yaml"
gh repo create render-demo --private --source=. --push

# Now go to render.com, connect the repo, and deploy
# Or use the CLI:
render deploy --yes

Visit your service URL + /health to confirm the response.


Quick Reference

# render.yaml service types
- type: web           # Web service (HTTP)
- type: worker        # Background worker (no HTTP)
- type: cron          # Scheduled job

# Common env var patterns
envVars:
  - key: NODE_ENV
    value: production
  - key: DATABASE_URL
    fromDatabase:
      name: my-db
      property: connectionString
  - key: SECRET
    generateValue: true
  - key: MANUAL_SECRET
    sync: false

# Plans
plan: starter         # 512MB RAM, 0.5 CPU (~$7/mo)
plan: standard        # 2GB RAM, 1 CPU (~$25/mo)
plan: pro             # 4GB RAM, 2 CPU (~$85/mo)

# Health check
healthCheckPath: /health

# Preview environments
previewsEnabled: true

# Scaling (Standard+ only)
scaling:
  minInstances: 1
  maxInstances: 5
  targetCPUPercent: 75
# Render CLI
render deploy --yes             # Apply render.yaml
render deploy --wait            # Wait for deploy to complete
render logs <service-name> --tail
render services list
render env list <service-name>
render env set <service-name> KEY=value

Further Reading