Deploying a full-stack app can feel like running air traffic control: frontend landing on Vercel, backend circling on Heroku, database parked elsewhere and you're struggling with CORS and separate domains (api.myapp.com vs myapp.com) just to keep everything in the air.
I recently deployed a production-grade Angular + NestJS + PostgreSQL stack to Google Cloud, and I want to share the exact setup. It allows you to:
- Host everything on a single domain (e.g.,
myapp.com/apihits your backend). - Get Free SSL and CDN via Firebase Hosting.
- Pay minimal costs (~$10-15/mo) for a real SQL database and serverless backend.
Here is the step-by-step guide to doing it right from scratch.
The Architecture
We are going to use a "Unified" approach:
- Frontend: Firebase Hosting (serves Angular static files).
- Backend: Google Cloud Run (runs NestJS container).
- Database: Cloud SQL (PostgreSQL).
- The Secret Sauce: Firebase Hosting Rewrites. This lets Firebase act as a reverse proxy, forwarding
/api/*traffic to your backend seamlessly. No CORS madness!
Step 1: Set up the Database (Cloud SQL)
We want a real PostgreSQL database, but we don't want to pay enterprise prices ($100+/mo).
- Enable Cloud SQL Admin API in your Google Cloud Project.
- Create a "Micro" instance. This is the trick to low costs:
gcloud sql instances create my-db-instance \
--project=[YOUR_PROJECT_ID] \
--database-version=POSTGRES_15 \
--tier=db-f1-micro \
--region=asia-south1 \ # Choose a region close to your users
--root-password="[STRONG_PASSWORD]"
Note: The db-f1-micro is a shared-core machine. It's perfect for low-to-medium traffic apps.
- Create your specific database inside that instance:
gcloud sql databases create my_database_name --instance=my-db-instance
Step 2: Containerize NestJS (The Backend)
Your NestJS code needs to be wrapped in a Docker container to run on serverless Cloud Run.
Optimized Dockerfile:
Use a multi-stage build to keep your image light.
# Build Stage
FROM node:18-alpine As build
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production Stage
FROM node:18-alpine
WORKDIR /usr/src/app
COPY --from=build /usr/src/app/dist ./dist
COPY --from=build /usr/src/app/node_modules ./node_modules
EXPOSE 8080
CMD ["node", "dist/main"]
Crucial Code Change (main.ts):
Cloud Run expects your app to listen on 0.0.0.0 (not localhost) and port 8080.
// main.ts
const port = process.env.PORT || 8080;
await app.listen(port, '0.0.0.0');
Step 3: Deploy Backend to Cloud Run
This one command builds your image, uploads it, and deploys it. It also securely connects your Cloud SQL instance using the Unix socket (fast & secure).
gcloud run deploy my-backend-service \
--source . \
--region asia-south1 \
--allow-unauthenticated \
--add-cloudsql-instances [PROJECT_ID]:[REGION]:[INSTANCE_NAME] \
--set-env-vars NODE_ENV=production \
--set-env-vars DATABASE_HOST=/cloudsql/[PROJECT_ID]:[REGION]:[INSTANCE_NAME] \
--set-env-vars DATABASE_USER=postgres \
--set-env-vars DATABASE_PASSWORD=[YOUR_PASSWORD] \
--set-env-vars DATABASE_NAME=my_database_name
Pro Tip: For your very first deployment, your database will be empty. You can temporarily add
--set-env-vars DB_SYNCHRONIZE=trueto force TypeORM to create your tables, then remove it immediately after!
Step 4: Configure Angular (The Frontend)
We need to tell Angular to simply call /api instead of a full URL.
-
Production Environment (
environment.prod.ts):
export const environment = { production: true, apiUrl: '/api' // Relative path is key! }; -
Local Development Proxy (
proxy.conf.json):
To make this work locally (solocalhost:4200/api->localhost:3000), add this file:
{ "/api": { "target": "http://localhost:3000", "secure": false } }And update
angular.jsonto use it:"proxyConfig": "proxy.conf.json"in your serve options.
Step 5: The Unification (Firebase Hosting)
This is where the magic happens. We tell Firebase: "Serve my Angular app for everything, BUT if someone asks for /api, send them to Cloud Run."
firebase.json:
{
"hosting": {
"public": "dist/my-app/browser",
"rewrites": [
{
"source": "/api/**",
"run": {
"serviceId": "my-backend-service",
"region": "asia-south1"
}
},
{
"source": "**",
"destination": "/index.html"
}
],
"headers": [
{
"source": "**/*.@(html|json)",
"headers": [
{
"key": "Cache-Control",
"value": "no-cache, no-store, must-revalidate"
}
]
}
]
}
}
Note the headers section: This disables caching for index.html, which is critical to ensure users always see your latest deployment immediately.
Step 6: Deploy Frontend
npm run build --configuration=production
firebase deploy
Conclusion
You now have:
- Frontend:
https://myapp.com - Backend:
https://myapp.com/api(Proxied internally) - Database: Securely connected private SQL instance.
No CORS errors. No complex load balancers. Just a clean, professional, and scalable architecture.
Top comments (0)