Back to Blog
Tutorial10 min read2026-06-30

How to Deploy a Full-Stack App with a Database

Shipping a full-stack app means coordinating a frontend, an API, and a database. This tutorial walks through the architecture decisions, environment wiring, and a clean deploy from git push to live URL.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

# How to Deploy a Full-Stack App with a Database

A full-stack app has three moving parts that all have to find each other: a frontend, a backend API, and a database. Getting them deployed and correctly wired — without committing secrets or fighting CORS — trips up a lot of developers. This tutorial walks through the architecture and a clean deployment.

The architecture

We will deploy a typical setup:

Browser → Frontend (React/Vite static) → API (Node/Express container) → PostgreSQL

Three deployable units:

  1. 1Frontend — a static SPA (or SSR app). Built once, served from the edge.
  2. 2Backend — a containerized API that talks to the database.
  3. 3Database — a managed PostgreSQL instance.

The key decision is keeping the database private — only the backend should reach it — and exposing only the frontend and the API to the internet.

Step 1: prepare the database layer

Your backend should read the database connection string from an environment variable, never hardcode it:

// db.js
import { Pool } from 'pg';

export const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
});

Keep schema changes in versioned migrations so any environment can be rebuilt deterministically:

-- migrations/001_init.sql
CREATE TABLE todos (
  id SERIAL PRIMARY KEY,
  title TEXT NOT NULL,
  done BOOLEAN NOT NULL DEFAULT false,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

Step 2: containerize the backend

A clean, production Dockerfile reads the port from the environment:

FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
ENV PORT=8080
EXPOSE 8080
CMD ["node", "server.js"]

Run migrations on startup or as a release step so the schema is always current before traffic arrives:

// run on boot, before listening
await runMigrations(pool);
app.listen(process.env.PORT || 8080);

Step 3: build the frontend

The frontend needs to know where the API lives. Inject the API base URL at build time:

# Vite reads VITE_-prefixed vars at build
VITE_API_URL=https://api.myapp.com npm run build
const api = import.meta.env.VITE_API_URL;
fetch(`${api}/todos`).then(r => r.json());

The build output (dist/) is static files — serve them from a CDN, not a server.

Step 4: handle CORS

Because the frontend and API are on different hostnames, the API must allow the frontend's origin:

import cors from 'cors';
app.use(cors({ origin: 'https://myapp.com', credentials: true }));

Misconfigured CORS is the single most common full-stack deploy failure. Set the exact origin, not *, when using credentials.

Step 5: wire the environment variables

Here is the full wiring map. Nothing sensitive lives in git.

ComponentVariableValue
BackendDATABASE_URLmanaged Postgres connection string
BackendPORTinjected by platform
BackendCORS_ORIGINhttps://myapp.com
Frontend (build)VITE_API_URLhttps://api.myapp.com

Step 6: deploy in the right order

  1. 1Database first — provision and run migrations.
  2. 2Backend next — deploy with DATABASE_URL set; confirm /health returns OK.
  3. 3Frontend last — build with the live API URL and deploy.
  4. 4Domains + SSL — attach myapp.com to the frontend and api.myapp.com to the backend; let the platform provision certificates.

A simple health check the platform can poll:

app.get('/health', async (_req, res) => {
  try { await pool.query('SELECT 1'); res.json({ ok: true }); }
  catch { res.status(503).json({ ok: false }); }
});

Common pitfalls

  • Database publicly exposed. Keep it private; only the backend connects.
  • Secrets in the frontend bundle. Anything in the client build is public. Only the API URL belongs there, never DB credentials.
  • Forgetting migrations in CI. Schema drift between environments causes mysterious 500s.
  • CORS wildcards with credentials. Browsers reject * + credentials.

Doing it on PandaStack

This is the workflow PandaStack is built around. Connect your repo and it auto-detects the frontend framework and the backend (Dockerfile or buildpacks), builds both, and goes live. Provision a managed PostgreSQL and PandaStack auto-wires itDATABASE_URL is injected into your backend, so step 1's connection string is handled for you. Custom domains get automatic SSL, you get live build/app logs to debug the first deploy, plus rollbacks and deploy history if something goes wrong. The free tier includes 5 web services, 5 static sites, and 1 database — enough for a complete full-stack app.

References

  • [PostgreSQL node-pg connection docs](https://node-postgres.com/features/connecting)
  • [MDN: Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
  • [Vite environment variables](https://vitejs.dev/guide/env-and-mode.html)
  • [The Twelve-Factor App: config](https://12factor.net/config)

---

Full-stack deploys are about ordering and wiring, not magic. PandaStack auto-wires your database and builds frontend and backend from one git push — start free at [dashboard.pandastack.io](https://dashboard.pandastack.io).

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also