Back to guides
🔥 New Guide⏱ ~1 hour📶 Intermediate

Migrate from Vercel to
DigitalOcean

10-second function timeouts. $20/mo minimum. Surprise bandwidth bills. A $6 DigitalOcean droplet runs your Next.js app faster, cheaper, with no limits. This guide walks you through it step by step — starting with pulling your env vars out of Vercel.

FeatureVercelDigitalOcean
Price$20-$40/mo (Pro)$6/mo droplet
Cold startsYes (serverless)None (always-on)
Function timeout10s Hobby / 60s ProNo limit
Bandwidth100GB then $0.40/GB1TB included
Env var exportvercel env pullYou own the .env file
Setup time5 min~1 hour (first time)
Preview deploysPer-PR, automaticManual setup needed
SSLAutomaticCertbot (2 min setup)
Vendor lock-inHigh (Edge, ISR)None — standard Linux

Start from Step 00 — don't skip it

Run vercel env pull before you do anything else. Your environment variables are the most critical thing to preserve — losing them means a broken app. Keep your Vercel project alive until everything works on DO.

00

Export everything from Vercel first

Before you delete anything, pull your environment variables and project config out of Vercel. One command does most of the work.

  • Install Vercel CLI if you don't have it: npm i -g vercel
  • Login: vercel login (opens browser)
  • Link to your project: vercel link (run from your project folder)
  • Pull ALL env vars to local file: vercel env pull .env.local
  • This creates a .env.local with every Production environment variable — the same ones Vercel injects at build time
  • Also note: Vercel → your project → Settings → General → copy your Framework Preset and Build Command
terminal / config
# Pull all your Vercel env vars in one command:
npm i -g vercel
vercel login
vercel link
vercel env pull .env.local

# Your .env.local now has every var Vercel was injecting.
# Keep this file — you'll need it for the next steps.
AI Prompt — copy into Claude or Cursor
I'm migrating my Next.js app from Vercel to DigitalOcean. I just ran "vercel env pull .env.local" and got all my environment variables. Help me: 1) Audit the .env.local file and identify which vars are NEXT_PUBLIC_ (baked at build time) vs server-only, 2) Create a .env.production file for the server with only the server-side vars, 3) Tell me which vars I'll need to pass to GitHub Actions as secrets for the build step.
01

Create your DigitalOcean Droplet + API token

Spin up your $6 server. Also grab a DigitalOcean API token — useful for automating DNS, firewall rules, and future deployments.

  • Go to digitalocean.com → Create → Droplets
  • Choose: Ubuntu 22.04 LTS, Basic, Regular CPU, $6/mo (1 vCPU, 1GB, 25GB SSD)
  • Authentication: paste your SSH public key (cat ~/.ssh/id_ed25519.pub) or set a root password
  • Pick a datacenter region close to your users, click Create Droplet
  • Wait ~60 seconds — copy the IP address shown in your dashboard
  • Now get an API token: digitalocean.com → API → Tokens → Generate New Token → Read + Write
  • Copy the token immediately — only shown once
terminal / config
# Add to .env.local (for reference)
DO_API_TOKEN=dop_v1_xxxx        # DigitalOcean API token
DO_DROPLET_IP=64.225.12.5       # your droplet IP

# SSH into server:
ssh root@64.225.12.5
AI Prompt — copy into Claude or Cursor
I have a fresh Ubuntu 22.04 DigitalOcean droplet at [YOUR_IP]. Set up the server for a Next.js app: 1) Update all packages, 2) Configure UFW firewall — allow only 22, 80, 443, 3) Install Node.js 20 via NodeSource, 4) Install PM2 globally, 5) Install Nginx. Give me all commands in one block I can paste into the terminal.
Create DigitalOcean Account ($200 credit)
02

Deploy your app with PM2

Clone your repo, install deps, build, and start with PM2. This is what Vercel was doing automatically — now you control it.

  • SSH into your droplet: ssh root@YOUR_IP
  • Clone your repo: git clone https://github.com/youruser/yourrepo /var/www/myapp
  • Create your .env.local on the server with your production variables
  • Install and build: cd /var/www/myapp && npm ci && npm run build
  • Start with PM2: pm2 start npm --name "myapp" -- start
  • Save PM2 config: pm2 save
  • Auto-start on reboot: pm2 startup (run the command it outputs)
  • Verify: curl http://localhost:3000 — should return your HTML
terminal / config
# Create .env.local on server without exposing in bash history:
cat > /var/www/myapp/.env.local << 'EOF'
DATABASE_URL=postgresql://...
JWT_SECRET=...
RESEND_API_KEY=...
# paste all your vars here
EOF
chmod 600 /var/www/myapp/.env.local

# Build and start
cd /var/www/myapp
npm ci && npm run build
pm2 start npm --name "myapp" -- start
pm2 save && pm2 startup
AI Prompt — copy into Claude or Cursor
I have a Next.js app at /var/www/myapp on Ubuntu 22.04. My .env.local has these variables: [PASTE YOUR VARS HERE - remove secret values].

Help me:
1) Create the .env.local on the server safely (without echoing secrets in bash history)
2) Run npm ci and npm run build
3) Start with PM2 and verify it's running
4) Configure PM2 to restart on server reboot
5) Check the app logs: pm2 logs myapp

The app should be accessible at http://localhost:3000 before I set up Nginx.
03

Configure Nginx as reverse proxy

Nginx sits in front of your Next.js app and handles traffic on port 80/443 — the same job Vercel's edge network was doing.

  • Create config file: nano /etc/nginx/sites-available/myapp
  • Paste the Nginx config below (replace yourdomain.com)
  • Enable it: ln -sf /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/myapp
  • Disable default: rm /etc/nginx/sites-enabled/default
  • Test: nginx -t (should say "syntax is ok")
  • Reload: systemctl reload nginx
  • Visit http://YOUR_IP — your app should load
terminal / config
/etc/nginx/sites-available/myapp

server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    # Cache Next.js static assets (content-hashed, safe to cache forever)
    location /_next/static/ {
        proxy_pass http://localhost:3000;
        add_header Cache-Control "public, max-age=31536000, immutable";
    }

    location / {
        proxy_pass http://localhost: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;
    }
}
AI Prompt — copy into Claude or Cursor
Create a production-ready Nginx config for my Next.js app. Requirements: domain is yourdomain.com and www.yourdomain.com, Next.js runs on port 3000, proxy all requests with proper headers (X-Real-IP, X-Forwarded-For, X-Forwarded-Proto), cache _next/static files for 1 year (they have content hashes), handle WebSocket upgrades. Also show me how to test the config before reloading.
04

Point your domain DNS + get free SSL

Update your domain's A record to point to your droplet IP, then Certbot gives you HTTPS in 2 minutes. Vercel did this automatically — Certbot makes it just as easy.

  • Go to your domain registrar (Namecheap, Cloudflare, etc.) → DNS settings
  • Add/update A record: @ → YOUR_DROPLET_IP (TTL: 5 min for faster propagation)
  • Add/update A record: www → YOUR_DROPLET_IP
  • Wait for DNS to propagate: dig yourdomain.com A +short (should return your IP)
  • Once DNS resolves: certbot --nginx -d yourdomain.com -d www.yourdomain.com
  • Follow the prompts — Certbot edits your Nginx config and reloads it
  • Test HTTPS: curl https://yourdomain.com — should return your app
  • SSL auto-renews every 90 days — certbot sets up a cron for this automatically
terminal / config
# Check DNS has propagated first:
dig yourdomain.com A +short
# Should return: YOUR_IP

# Get SSL certificate:
certbot --nginx -d yourdomain.com -d www.yourdomain.com

# Test auto-renewal:
certbot renew --dry-run

# Certbot automatically edits your Nginx config to add HTTPS
# and sets up HTTP → HTTPS redirect
AI Prompt — copy into Claude or Cursor
I updated my domain DNS A record to point to [YOUR_IP] and DNS has propagated. Walk me through: 1) Running Certbot to get SSL for yourdomain.com and www.yourdomain.com, 2) Verifying the cert was issued correctly, 3) Setting up a HTTP → HTTPS redirect, 4) Testing auto-renewal works. My server is Ubuntu 22.04 with Nginx already installed.
05

Set up GitHub Actions auto-deploy

Every git push to main automatically builds and ships to your server. The same developer experience as Vercel, on your own infrastructure.

  • On your server: generate a deploy key: ssh-keygen -t ed25519 -f ~/.ssh/github_deploy -N ""
  • Add to authorized_keys: cat ~/.ssh/github_deploy.pub >> ~/.ssh/authorized_keys
  • Copy the private key: cat ~/.ssh/github_deploy (you'll need this in a moment)
  • Go to your GitHub repo → Settings → Secrets and variables → Actions
  • Add secret DO_SSH_KEY: paste the full private key content
  • Add secret DO_HOST: your droplet IP
  • Add secret DO_USER: root (or your server username)
  • Add all your app env vars as secrets too (from your .env.local)
  • Create .github/workflows/deploy.yml — see the workflow file below
terminal / config
# .github/workflows/deploy.yml
name: Deploy
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'
      - run: npm ci
      - run: npm run build
        env:
          NEXT_PUBLIC_APP_URL: ${{ secrets.NEXT_PUBLIC_APP_URL }}
          # add all your NEXT_PUBLIC_ vars here
      - uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.DO_HOST }}
          username: ${{ secrets.DO_USER }}
          key: ${{ secrets.DO_SSH_KEY }}
          script: |
            cd /var/www/myapp
            git pull origin main
            npm ci --omit=dev
            npm run build
            pm2 restart myapp
      - name: Health check
        run: sleep 10 && curl -f https://yourdomain.com/ || exit 1
AI Prompt — copy into Claude or Cursor
Create a GitHub Actions deploy workflow for my Next.js app on DigitalOcean. Requirements:
- Trigger on push to main branch
- Build Next.js on the CI runner (NOT on the 1GB RAM server)
- Pass these env vars to the build step from GitHub Secrets: [LIST YOUR NEXT_PUBLIC_ VARS]
- After build succeeds: SSH into server, git pull, npm ci --omit=dev, pm2 restart myapp
- Add health check: curl -f https://yourdomain.com/ || exit 1
- Use secrets: DO_HOST, DO_USER, DO_SSH_KEY

Show me the complete .github/workflows/deploy.yml
06

Move your environment variables to GitHub Secrets

Vercel had a UI for env vars. GitHub Actions Secrets is your new UI — and once they're there, every deploy picks them up automatically.

  • You already have your vars in .env.local (from Step 00)
  • Split them into two groups: NEXT_PUBLIC_* (needed at build time) and server-only (needed at runtime)
  • Add every NEXT_PUBLIC_* var to GitHub Secrets — the Actions workflow passes them to npm run build
  • Add server-only vars directly to /var/www/myapp/.env.local on the server (not in GitHub Secrets)
  • To update a server-side var: SSH in, edit .env.local, run pm2 restart myapp
  • Never commit .env files to git — make sure .env* is in your .gitignore
terminal / config
# NEXT_PUBLIC_ vars → GitHub Secrets (used at build time in Actions)
# Add to repo → Settings → Secrets → Actions:
NEXT_PUBLIC_APP_URL=https://yourdomain.com

# Server-only vars → .env.local on server (runtime, not in git)
# SSH in and edit:
nano /var/www/myapp/.env.local
# Then: pm2 restart myapp

# .gitignore — make sure this is there:
.env
.env.local
.env.production
.env*.local
AI Prompt — copy into Claude or Cursor
I have a Next.js app with these environment variables (example structure, not real values):
- NEXT_PUBLIC_APP_URL=https://... (needed at build time)
- NEXT_PUBLIC_POSTHOG_KEY=phc_... (needed at build time)
- DATABASE_URL=postgresql://... (server only)
- JWT_SECRET=... (server only)
- RESEND_API_KEY=re_... (server only)

Help me: 1) Which vars go in GitHub Secrets vs server .env.local, 2) How to update the GitHub Actions workflow to pass NEXT_PUBLIC_ vars to the build step, 3) How to safely update server-only vars without redeploying the whole app.
📦

Using Vercel Blob for file storage? → Use Backblaze B2

Vercel Blob is S3-compatible storage. Backblaze B2 does the same thing at $0.006/GB — 10x cheaper than AWS S3, and your code barely changes since B2 has an S3-compatible API.

Read the B2 Storage Guide →
🎉

You escaped Vercel.

No cold starts, no 10-second limits, no $20/mo minimum. Your app runs on a $6 server that you fully own.