The Ephemeral PR Environments Proof of Concept (POC) repository demonstrates a highly advanced, cost-effective, and scalable approach to spinning up complete microservice environments dynamically for every GitHub Pull Request.
The primary goal of this architecture is to provide Isolated Testing for every single pull request, inspired by the modern "Preview Branches" paradigm.
When a developer works on a feature, they shouldn't have to share a staging environment where their disruptive database migrations or experimental code might block other engineers. They need a complete, sandboxed environment tailored exactly to their git branch.
However, traditionally spinning up a full, isolated replica (with dedicated databases and compute clusters) for every PR is incredibly expensive and slow.
This POC solves the cost-speed barrier to isolated testing by implementing a Hybrid Cloud Architecture:
- Serverless Compute (Zero Idle Cost): The actual microservices (Frontend, Order, Inventory, Notification) deploy to Google Cloud Run. They scale to zero when not actively being tested.
- Shared State (Low Cost Anchor): All databases (Postgres, MySQL, MongoDB) run on a single, affordable Compute Engine VM.
- Dynamic Routing (The Magic): We only deploy the services that changed in the PR. For any service that didn't change, we dynamically route traffic back to the persistent staging environment.
A high-level view of our Hybrid Cloud architecture showing Serverless Cloud Run communicating with the Stateful Staging VM via Serverless VPC Access.
- Frontend: React + Vite + TailwindCSS
- Backend Services: NestJS + Prisma ORM
- Databases: PostgreSQL, MySQL, MongoDB
- Infrastructure: Google Cloud Platform (Cloud Run, Compute Engine, Serverless VPC Access)
- CI/CD: GitHub Actions (Self-hosted runner)
Instead of deploying a full clone of the infrastructure for every PR, we utilize intelligent header propagation to dynamically route requests only to the services that were modified in the PR. All other requests seamlessly fall back to the persistent staging environment.
The flow of an HTTP request as it originates from the QA engineer's browser and dynamically weaves between isolated PR instances and stable staging fallbacks.
Imagine a developer opens a PR (#5) that modifies the order-service and inventory-service, but leaves the notification-service completely untouched:
- The Entrypoint (Frontend UI):
- The CI/CD pipeline comments on the PR with a special dynamic URL containing query parameters for the modified services:
https://frontend-pr-5-xyz.run.app/?order_pr=5&inventory_pr=5
- The CI/CD pipeline comments on the PR with a special dynamic URL containing query parameters for the modified services:
- Frontend Interceptor (Outbound to Backend):
- When the reviewer clicks the link, the React frontend extracts these parameters.
- An Axios interceptor automatically attaches them as custom HTTP headers to all outgoing API calls:
X-Order-PR: 5X-Inventory-PR: 5
- Because
order_pr=5is present, the frontend dynamically targets the ephemeralorder-serviceCloud Run URL instead of the staging URL.
- Backend Orchestrator (Order Service):
- The ephemeral
order-service-pr-5receives the request. A NestJS middleware extracts the customX-*-PRheaders and stores them securely in Node'sAsyncLocalStorage, making them available globally for the lifecycle of that specific request. - The core business logic in the
order-serviceexecutes normally, completely unaware of the ephemeral setup. It simply attempts to call the inventory service using the standard, hardcoded staging URL:POST http://inventory-svc.ephemeral-poc.run.place/inventory/deduct
- The ephemeral
- Backend Interceptor (
PrHttpModule):- Right before the HTTP request leaves the
order-service, our centralizedPrHttpModuleinterceptor catches it. - It checks
AsyncLocalStorageand sees theX-Inventory-PR: 5header. - It dynamically rewrites the destination URL on the fly:
From:
http://inventory-svc.ephemeral-poc.run.place/inventory/deductTo:https://inventory-service-pr-5-xyz.us-central1.run.app/inventory/deduct
- Right before the HTTP request leaves the
- The Staging Fallback (Notification Service):
- Later in the execution, the
order-serviceattempts to call thenotification-service. - The
PrHttpModuleintercepts it, but sees there is noX-Notification-PRheader (because the notification service didn't change in this PR). - It leaves the URL completely untouched, and the request is safely routed to the stable, always-on staging
notification-service.
- Later in the execution, the
This architecture creates a massive cost reduction, as PRs only provision cloud compute for exactly what they changed, while still behaving like a complete, isolated environment for the reviewer.
When a backend service changes, we need an isolated database to run schema migrations and tests without breaking the staging environment.
Our GitHub Actions pipeline connects to the staging VM and executes a highly optimized, idempotent clone_db.sh script. This script dynamically clones the staging database schema and data into a temporary PR namespace (e.g., order_db_pr_5) in milliseconds, without provisioning any new hardware.
To allow Cloud Run services to securely talk to the internal VM databases, we use a Serverless VPC Access Connector. Cloud Run is configured with --vpc-egress private-ranges-only. This ensures database traffic stays securely inside the private VPC (10.x.x.x), while inter-service communication (Cloud Run to Cloud Run) routes securely over the public internet.
When a Pull Request is merged or closed, a cleanup GitHub Action automatically:
- Deletes the ephemeral Cloud Run services to stop billing.
- Connects to the VM and drops the isolated PR databases to free up storage space.
Here is a step-by-step visual walkthrough of the Ephemeral PR Environments in action based on our POC:
Docker images for all microservices are successfully built and pushed to the Google Cloud Artifact Registry.
The self-hosted GitHub Runner, Docker Compose stack, and local .env files are fully configured and running securely on the Staging VM.
A developer opens a new Pull Request. This instantly triggers the GitHub Actions CI/CD pipeline to evaluate changes and spin up targeted ephemeral resources.
Once the pipeline finishes, the GitHub Action automatically posts a comment on the PR containing the dynamic, clickable preview URLs for QA testing.
Clicking the preview link opens the Frontend hosted on a completely isolated Cloud Run instance, dynamically injecting PR tracking headers.
The Order Service successfully intercepts the PR headers and communicates with its own cloned, isolated PostgreSQL database.
The Inventory Service is dynamically targeted by the Order Service and correctly interacts with its own isolated MySQL database clone.
The Notification Service successfully processes requests in its ephemeral Cloud Run container using its cloned MongoDB database.
When the Pull Request is merged or closed, a cleanup pipeline automatically deletes the ephemeral Cloud Run instances and drops the isolated databases, reducing costs to zero.
If you want to recreate this infrastructure from scratch, we have created a comprehensive, step-by-step setup guide.
π Read the Infrastructure Setup Guide
If you want to understand exactly how the GitHub Actions pipelines detect changes, clone databases, and deploy to Cloud Run: π Read the CI/CD Workflows Architecture
.
βββ .github/
β βββ workflows/ # GitHub Actions CI/CD pipelines
βββ app/
β βββ frontend/ # React Vite Frontend
β βββ inventory-service/ # NestJS + MySQL
β βββ notification-service/ # NestJS + MongoDB
β βββ order-service/ # NestJS + Postgres
βββ infra/
β βββ nginx/ # Reverse Proxy configuration
β βββ docker-compose.staging.yml # Anchor VM Database Stack
βββ scripts/ # Automated database cloning & cleanup scripts
- VM Access Scopes: By default, GCP limits VM access scopes. To allow a self-hosted GitHub runner to delete Cloud Run services, the VM must be explicitly configured with "Allow full access to all Cloud APIs".
- MongoDB in Docker: MongoDB requires a stable hostname for replica sets. Hardcoding
mongo-db:27017in the replica set initialization preventsRsGhosterrors when containers restart and get new Docker IDs. - Idempotency is Key: CI/CD pipelines must check if a PR database already exists before cloning. This prevents developers from losing their test data when pushing consecutive commits to the same PR.