5+ years software engineer
5+ years software engineer
5+ years software engineer
5+ years software engineer
Deploying a full-stack app comes down to five moving parts in the right order: get your environment production-ready, deploy the database, deploy the backend, deploy the frontend, then connect them over HTTPS with correct CORS. Do it in that sequence and each step has what it needs from the one before. Below is the exact process I follow, with a MERN example you can copy.
Most deploy failures trace back to work skipped here. Before touching a host, get three things right.
No hardcoded secrets. Every URL, key, and credential moves to environment variables. If a database password is sitting in your source, it's already compromised the moment the repo is shared. Use a .env file locally (gitignored) and your host's secret manager in production.
bash1# .env.local — never commit this 2DATABASE_URL="mongodb+srv://user:pass@cluster.mongodb.net/app" 3JWT_SECRET="a-long-random-string" 4NODE_ENV="production"
A real production build. Development servers are not production servers. Run and test the actual build locally first — it'll catch missing env vars and build-time errors before your host does.
bash1npm run build # produces the optimized bundle 2npm start # run the production build locally to smoke-test it
A production checklist. I keep a short one and run it every time:
.env files gitignoredThere's no single right answer, but the modern default splits into three layers. Here's what I reach for:
| Layer | Options | I usually pick |
|---|---|---|
| Frontend | Vercel, Netlify, Cloudflare Pages | Vercel (esp. for Next.js) |
| Backend | Railway, Render, Fly.io, AWS | Railway or Render for speed; AWS for control |
| Database | Managed Postgres, MongoDB Atlas | Atlas (Mongo) / Neon or RDS (Postgres) |
The principle: use managed services for the database, always. Running your own database on a raw VM means you own backups, failover, patching, and scaling. A managed provider does all of that for a few dollars a month. It's the single best money-for-time trade in deployment.
The database has no dependencies, so it goes first — everything else needs its connection string.
For MongoDB Atlas: create a cluster, add a database user, and configure network access. In production you'd lock this to your backend's IP; while getting started, 0.0.0.0/0 works but tighten it before you have real data. Grab the connection string — that becomes your backend's DATABASE_URL.
Point your backend host (Railway/Render/Fly) at your repo, set the environment variables — including the database URL from Step 3 — and let it build. Two things that trip people up:
PORT env var; hardcoding 3000 will fail.NODE_ENV=production.js1const port = process.env.PORT || 3000; 2app.listen(port, () => console.log(`API listening on ${port}`));
Once it's live, hit a health endpoint (GET /api/health) to confirm the backend and database are talking before you move on.
Connect your frontend repo to Vercel/Netlify. The one critical env var here is the backend's public URL, so the frontend knows where to send API calls:
bash1# Frontend environment variable 2VITE_API_URL="https://my-api.up.railway.app" 3# or for Next.js 4NEXT_PUBLIC_API_URL="https://my-api.up.railway.app"
This is where a first deploy usually breaks: the frontend loads, but every API call fails with a CORS error. Your backend must explicitly allow requests from the frontend's origin.
js1import cors from "cors"; 2 3app.use(cors({ 4 origin: process.env.FRONTEND_URL, // e.g. https://myapp.vercel.app 5 credentials: true, // needed if you send cookies 6}));
Don't ship origin: "*" with credentials — browsers reject it, and it's a security hole. Name the exact origin.
Point your domain's DNS at your frontend host (a CNAME or the host's provided records). Every host in this stack provisions a free TLS certificate automatically, so HTTPS is handled — you just wait for DNS to propagate. Do the same for your API subdomain (e.g. api.myapp.com) and update the frontend's API_URL to match.
Load the live site and exercise the real flows: sign up, log in, create data, refresh. Open the browser network tab and confirm API calls hit the production backend over HTTPS with no CORS errors. Check your host's logs for runtime errors.
Here's how the pieces connect in a typical MERN deploy — Vercel frontend, Railway backend, MongoDB Atlas:
┌─────────────────┐
User's browser ──HTTPS──▶ Vercel │ React/Vite frontend
│ (myapp.com) │ env: NEXT_PUBLIC_API_URL
└────────┬─────────┘
│ fetch() over HTTPS (CORS-allowed origin)
▼
┌─────────────────┐
│ Railway │ Node/Express API
│ (api.myapp.com) │ env: DATABASE_URL, JWT_SECRET
└────────┬─────────┘
│ MongoDB connection string (TLS)
▼
┌─────────────────┐
│ MongoDB Atlas │ managed database
│ (cluster) │ network access locked to API
└─────────────────┘
A few problems account for the vast majority of "it works locally but not in production" tickets. Knowing them in advance saves hours:
| Symptom | Usual cause | Fix |
|---|---|---|
| CORS error on every API call | Backend doesn't allow the frontend origin | Set origin to the exact frontend URL |
| Backend crashes on boot | Hardcoded port; missing env var | Use process.env.PORT; set all env vars on the host |
| API calls 404 or hit localhost | Frontend API_URL still points at dev | Set the production API URL as a build-time env var |
| Database connection refused | Network access not allowing the backend | Allow the backend's IP in the DB's network rules |
| Works, then 500s under load | No connection pooling / cold DB | Use a pooled client; keep the DB warm |
| Secrets exposed | .env committed to the repo | Gitignore it, rotate the leaked secret immediately |
The theme is consistent: production differs from local in exactly the places you took shortcuts — ports, origins, URLs, and secrets. Get those into environment variables and most of this disappears.
Deploying once by hand is a great way to learn. Deploying by hand every time is how mistakes happen. The natural next step is a pipeline that runs your tests and deploys automatically on every merge — I cover that in How to Set Up CI/CD for a Web Application.
This article is part of my larger guide on Deploying, Scaling & Architecting Full-Stack Apps, which covers where deployment fits alongside performance and architecture.
A practical map for taking a full-stack app from a working codebase to a production system that ships reliably and scales — deployment, CI/CD, performance, and architecture choices.
Build a CI/CD pipeline that makes deploys boring and safe — the full flow from git push to production, a working GitHub Actions YAML file, deployment strategies, and the best practices that keep pipelines fast and reliable.
A priority-ordered playbook for making web apps fast — measure Core Web Vitals first, then reduce JavaScript, optimize images, fix caching, tune the backend, and cut re-renders. Based on real high-traffic optimization work.