Back to articles
devops July 15, 2026 7 min read

How to Deploy a Full-Stack App for Free

A step‑by‑step guide to getting a React/Node app live on Vercel, Render (or Railway), and a free Postgres instance—plus the real limits you’ll hit on a zero‑cost stack.

How to Deploy a Full‑Stack App for Free

Deploying a modern web app doesn’t have to cost a dime, but the free tiers you rely on come with quirks. In this post I’ll walk through a realistic, production‑ready stack that stays within the $0 budget:

I’ll show the exact configuration files, environment‑variable handling, and the commands you run locally. Then we’ll discuss the inevitable trade‑offs—cold starts, sleep timers, storage caps, and rate limits—so you know what you’re signing up for.

---

1. Pick Your Stack Components

| Layer | Free Provider | Typical Limits | |------|---------------|----------------| | Frontend | Vercel | 100 GB bandwidth / month, 12 months of serverless‑function runtime per month, 125 MB per function bundle | | Backend | Render (or Railway) | 750 hrs/month of container runtime, 512 MB RAM, 0.5 CPU, sleeps after 15 min of inactivity | | Database | Railway Postgres (or Supabase) | 500 MB storage, 5 GB data transfer, 1‑row per second write limit |

All three providers have a generous free tier that covers hobby projects, demos, and even low‑traffic production sites.

---

2. Scaffold a Minimal Full‑Stack App

We’ll use a classic Next.js front‑end that talks to an Express API backed by PostgreSQL.

# Create the repo root
mkdir fullstack-free && cd fullstack-free

# Frontend (Next.js)
npx create-next-app@latest frontend --use-npm --ts=false

# Backend (Express)
mkdir backend && cd backend
npm init -y
npm install express pg dotenv cors

2.1 Backend Boilerplate (backend/index.js)

require('dotenv').config();
const express = require('express');
const cors = require('cors');
const { Pool } = require('pg');

const app = express();
app.use(cors());
app.use(express.json());

// Connection pool – reads DATABASE_URL from env
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

app.get('/api/health', (req, res) => {
  res.json({ status: 'ok' });
});

app.get('/api/users', async (req, res) => {
  try {
    const { rows } = await pool.query('SELECT id, name FROM users ORDER BY id');
    res.json(rows);
  } catch (e) {
    console.error(e);
    res.status(500).json({ error: 'db error' });
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`🚀 API listening on ${PORT}`));

2.2 Sample .env.example

# Database URL supplied by Railway/Postgres
DATABASE_URL=postgres://user:password@host:5432/dbname

# Optional – change the port if you run locally on something else
PORT=3000

Commit everything except the real .env file.

---

3. Deploy the Backend on Render

3.1 Create a Render Service

1. Sign in to <https://render.com> and click New → Web Service. 2. Connect your GitHub repo (the same repo that contains the backend/ folder). 3. Set Root Directory to backend. 4. Choose Node as the environment. 5. Set Build Command to npm install. 6. Set Start Command to node index.js. 7. Add an Environment Variable called DATABASE_URL (we’ll fill it later). 8. Click Create Web Service.

Render will spin up a small container (512 MB RAM, 0.5 CPU). The first deployment may take ~30 seconds while the build runs.

3.2 Hook Up a Free Postgres Instance

1. In Render, click New → PostgreSQL. 2. Accept the default plan (Free – 500 MB). 3. After creation, copy the generated Internal Database URL. 4. Paste that value into the backend service’s DATABASE_URL env var and redeploy.

3.3 Optional: render.yaml for CI/CD

If you prefer a declarative config, add a render.yaml at the repo root:

services:
  - type: web
    name: backend
    env: node
    repo: https://github.com/yourname/fullstack-free
    branch: main
    rootDir: backend
    buildCommand: npm install
    startCommand: node index.js
    envVars:
      - key: DATABASE_URL
        fromDatabase: postgres-db # name you gave the DB service
    autoDeploy: true

Render will read this file on every push and automatically rebuild.

---

4. Deploy the Frontend on Vercel

4.1 Connect the Repo

1. Go to <https://vercel.com> and click New Project. 2. Import the same GitHub repo. 3. Vercel detects the frontend folder as a Next.js app. 4. Set Root Directory to frontend. 5. In Environment Variables, add NEXT_PUBLIC_API_URL pointing to the Render backend URL (e.g., https://backend-on-render.onrender.com). 6. Click Deploy.

4.2 Frontend Code (frontend/pages/index.js)

import { useEffect, useState } from 'react';

export default function Home() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/users`)
      .then(res => res.json())
      .then(data => {
        setUsers(data);
        setLoading(false);
      })
      .catch(err => console.error(err));
  }, []);

  if (loading) return <p>Loading…</p>;

  return (
    <div>
      <h1>Free‑Tier Users</h1>
      <ul>
        {users.map(u => (
          <li key={u.id}>{u.name}</li>
        ))}
      </ul>
    </div>
  );
}

4.3 Optional vercel.json

If you need custom redirects or rewrites, add a vercel.json in frontend/:

{
  "rewrites": [
    { "source": "/api/:path*", "destination": "${process.env.NEXT_PUBLIC_API_URL}/api/:path*" }
  ]
}

Vercel will automatically pick it up during the build.

---

5. Verify the End‑to‑End Flow

1. Open the Vercel preview URL (e.g., https://frontend‑xyz.vercel.app). 2. You should see the list of users fetched from the Render API. 3. Check the Render logs (Dashboard → Services → Logs) for any connection errors.

If you hit a CORS error, make sure the Express app uses cors() (already added) and that the frontend URL is allowed. Render’s free tier automatically adds the domain to the allowed origins when you enable the CORS middleware.

---

6. The Real‑World Trade‑offs of Free Tiers

6.1 Cold Starts & Sleep Timers

6.2 Storage & Data Limits

6.3 Rate Limits & Concurrency

6.4 Monitoring & Alerts

Free tiers provide basic logs but lack advanced metrics or alerting. Set up a simple health‑check endpoint (/api/health) and use a free monitoring service like UptimeRobot to ping it every 5 minutes.

6.5 Vendor Lock‑in

Because each provider expects you to stay within its ecosystem (e.g., Vercel’s next.config.js optimizations, Render’s render.yaml), migrating later can be non‑trivial. Keep your code as framework‑agnostic as possible and store config in environment variables, not provider‑specific files.

---

7. Tips for Making the Free Stack Feel More “Production‑Ready"

---

8. Wrap‑Up

You now have a fully functional full‑stack application running entirely on free tiers:

1. Frontend – Vercel builds and serves a Next.js app. 2. Backend – Render hosts an Express API, sleeping when idle. 3. Database – Railway provides a 500 MB Postgres instance.

The stack is perfect for portfolios, demos, or side‑projects, but be mindful of cold starts, storage caps, and limited monitoring. When your traffic outgrows those constraints, the migration path is clear: bump the Render plan, add a managed Postgres provider, or move the API to a dedicated cloud function platform.

Happy deploying!

Need something like this built?

I work on full-stack web apps — backend systems, APIs, and the front-ends that sit on top. If this post was useful and you've got a project that needs it, I'd like to hear about it.

Want future posts like this?

No mailing list yet — for now, email me and I'll let you know when something new goes up.

Email me