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:
- Frontend – Vercel (ideal for Next.js, static sites, or any SPA)
- Backend – Render (or Railway) running a Node/Express API
- Database – Free Postgres instance on Railway (or Supabase)
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
- Render: Containers go to sleep after 15 minutes of inactivity. The first request after sleep incurs a cold start (usually 2–5 seconds). For a low‑traffic blog this is fine; for an API that must respond instantly you’ll need a paid plan or a keep‑alive ping service.
- Vercel Serverless Functions: Functions also cold‑start, but Vercel’s edge network caches the result for a few seconds. Expect ~200 ms latency on a warm function, ~1 s on a cold one.
6.2 Storage & Data Limits
- Postgres (Railway): 500 MB of storage and a 5 GB/month outbound transfer cap. Large media files should live on a CDN or object storage (e.g., Cloudflare R2 free tier) rather than the DB.
- Vercel Build Output: Max 100 MB per deployment. Keep assets under this limit or host them elsewhere.
6.3 Rate Limits & Concurrency
- Render caps CPU at 0.5 cores, which means heavy computation (image processing, PDF generation) will be throttled. Use a background worker service (e.g., Railway’s “Jobs” feature) if you need more processing power.
- Vercel limits 125 ms of execution time for Edge Functions; for longer tasks you must fall back to a serverful endpoint (Render) or a queue.
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"
- Add a CDN: Vercel already serves static assets from its edge network. For dynamic images, use Cloudflare Images (free tier up to 100 GB stored).
- Use a Keep‑Alive Ping: A GitHub Action that hits the Render endpoint every 10 minutes prevents the container from sleeping.
- Database Backups: Railway provides automated daily snapshots, but they are retained only 7 days on the free plan. Export critical data to a CSV in a GitHub repo as a manual safety net.
- Security: Never commit
.envfiles. Use Render/Vercel’s secret management UI, and enable HTTPS (both providers force it by default).
---
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!