DEV Community

Cover image for Deploy Angular + NestJS to Google Cloud for Cheap (Single Domain, SSL, & Zero Headache)
Prasun Chakraborty
Prasun Chakraborty

Posted on

Deploy Angular + NestJS to Google Cloud for Cheap (Single Domain, SSL, & Zero Headache)

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:

  1. Host everything on a single domain (e.g., myapp.com/api hits your backend).
  2. Get Free SSL and CDN via Firebase Hosting.
  3. 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).

  1. Enable Cloud SQL Admin API in your Google Cloud Project.
  2. 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]"
Enter fullscreen mode Exit fullscreen mode

Note: The db-f1-micro is a shared-core machine. It's perfect for low-to-medium traffic apps.

  1. Create your specific database inside that instance:
gcloud sql databases create my_database_name --instance=my-db-instance
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Pro Tip: For your very first deployment, your database will be empty. You can temporarily add --set-env-vars DB_SYNCHRONIZE=true to 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.

  1. Production Environment (environment.prod.ts):

    export const environment = {
      production: true,
      apiUrl: '/api' // Relative path is key!
    };
    
  2. Local Development Proxy (proxy.conf.json):
    To make this work locally (so localhost:4200/api -> localhost:3000), add this file:

    {
      "/api": {
        "target": "http://localhost:3000",
        "secure": false
      }
    }
    

    And update angular.json to 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"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Conclusion

You now have:

  1. Frontend: https://myapp.com
  2. Backend: https://myapp.com/api (Proxied internally)
  3. Database: Securely connected private SQL instance.

No CORS errors. No complex load balancers. Just a clean, professional, and scalable architecture.

Top comments (0)