Migrating SynapseOne from Render to GCP: why we ditched PaaS for a VM with Docker Compose
A few months ago, SynapseOne ran on Render. It worked fine for the early stages. But as our clients’ data volume grew and we started designing AI-powered demand forecasting modules, we hit the hard reality of PaaS environments: lack of low-level system control and rigid hardware limits.
This article tells the story of our migration to Google Cloud Platform (GCP). It’s not a generic tutorial — it’s an honest technical log of how we decided to move a modern Elixir / Ash Framework multi-tenant stack to our own infrastructure, cutting costs and gaining full control over the processor.
The Problem with Render: The Glass Ceiling
Render is excellent for validating ideas. You configure it in 10 minutes, push to git, and you’re done. However, for a SaaS with data-intensive analytics, we started hitting friction:
- No low-level access: You can’t inspect kernel behavior live or tune Linux kernel parameters.
- Scaling costs: Hardware upgrades in managed environments financially penalize early-stage startups.
- Limited network isolation: We needed full flexibility to manage isolated containers and local data volumes.
Our Philosophy: Consolidated Architecture on GCP (Avoiding the Cost Trap)
When you research cloud migrations, traditional documentation pushes you to buy a thousand separate services: serverless instances (Cloud Run), ultra-managed databases (Cloud SQL), and distributed storage. For a startup, this is a recipe for inflated bills with hidden network transfer costs.
Our senior engineering decision was to go against the grain: Consolidate our entire stack inside a dedicated Compute Engine instance (a bare VM running Debian Linux) orchestrated with Docker Compose.
The Real Architecture
- Web Server (Phoenix + Bandit container): Handles concurrent app requests and multi-tenant schemas managed by Ash Framework.
- Database (Local PostgreSQL 16 container): Instead of paying for an external Cloud SQL service, we run PostgreSQL locally with persistent disk volumes. Network latency: minimal.
- Surgical Memory Management: We configured 2 GB of swap at the OS level as a safety cushion for data processing spikes and bulk report generation.
Cost Comparison (Why a VM Wins)
| Service | Cost/month | Why |
|---|---|---|
| Render (Starter) | $7 | Staging, 512MB RAM |
| Render (Standard) | $25 | Production, 2GB RAM |
| Render PostgreSQL | $6 | 256MB, shared |
| Total Render | $46/mo | No HW control |
| GCP Compute Engine (e2-small) | ~$18/mo | 2 vCPU, 2GB RAM, (+2GB swap) |
| GCP Postgres (container) | $0 | Included in the VM |
| Total GCP | ~$25/mo | 50% cheaper! |
Note: These costs apply to a startup profile with 1-10 active organizations. At larger scale, costs may vary, but a VM’s flexibility lets you adjust resources without being tied to fixed plans.
The Deployment Pipeline (Automated CI/CD)
Forget heavy interfaces. We designed a lean deployment pipeline using GitHub Actions and an automated Makefile:
-
On
git pushto production, GitHub builds the BEAM packaged release and stores it in the private GitHub Container Registry (GHCR). - GitHub Actions connects securely via SSH to our GCP virtual machine.
-
The server runs our automated Makefile task (
make prod-up), pulling only the updated web image, stopping the old container, and starting the new one in under a second — without interrupting or touching the local database.
The Makefile
COMPOSE_PROD=docker-compose.prod.yml
ENV_PROD=.env.prod
prod-pull:
docker compose --env-file $(ENV_PROD) -f $(COMPOSE_PROD) pull web
prod-up: prod-pull
docker compose --env-file $(ENV_PROD) -f $(COMPOSE_PROD) up -d
prod-migrate:
docker compose --env-file $(ENV_PROD) -f $(COMPOSE_PROD) \
exec web /app/bin/app rpc "Release.migrate()"
Real-World Challenges: The 50% CPU Mystery
No migration is perfect. When we spun up the GCP VM, we noticed the monitoring graph showed a flat 50% CPU usage at idle — even though our Elixir app reported under 1% usage.
Through fine-grained SSH debugging, we found the culprit: Google’s default telemetry agent (Cloud Ops Agent) was stuck in an infinite loop trying to send metrics without proper IAM permissions configured in the console.
The solution? Hot surgery on systemd:
sudo systemctl stop google-cloud-ops-agent
sudo systemctl disable google-cloud-ops-agent
The result was immediate: the graph broke through the 50% floor and dropped to a true 0% idle usage, freeing up all thermal processor capacity for our tenants’ concurrent queries.
Multi-tenancy with Ash Framework
We maintain our data isolation strategy using separate schemas per organization with Ash Multi-tenancy (org_<uuid>). By running Postgres locally and optimized, the engine handles concurrent transactions from different clients natively, fast, and without memory degradation.
The Real Superpower: IEx Console in Production and Hot Testing
If you code in Elixir, you know that one of the greatest gems of the Erlang Virtual Machine (BEAM) is the ability to connect interactively to your production application through a remote IEx (Interactive Elixir) session.
On Render, this was a forbidden dream. Due to the rigid hardware limits of the basic plan (512MB/1GB), attempting to spawn an interactive iex process alongside the web container under real traffic was a death sentence: the OOM (Out Of Memory) Killer in Linux would wake up and take your server down immediately. We lived blind, depending solely on structured logs.
By migrating to our consolidated infrastructure on GCP and strategically configuring 2GB of Swap, we gained the memory cushion needed to unlock the ultimate workflow.
Now, when we need to validate the behavior of an Artificial Intelligence module with real data, inspect a stuck Oban process, or perform hot tests without disrupting service to our clients, we simply run one command via SSH:
make prod-remote
Or directly:
docker compose --env-file $(ENV_PROD) -f $(COMPOSE_PROD) exec web /app/bin/app remote
This drops us straight into the heart of the live application. We can query repositories, launch test functions with Ash Framework, and debug in real-time with the confidence that the hardware has the budget to handle the load. Moving from the blindness of a hyper-constrained PaaS environment to the freedom of hot Elixir console has completely changed our response speed and engineering quality.
Conclusion
If you’re validating a prototype with a handful of users, start with Render. It’s simple and removes friction.
But if your SaaS starts processing real data volumes and you need to squeeze every processor cycle, the answer isn’t jumping to complex architectures like Kubernetes or Cloud Run that add unnecessary cost layers. A clean virtual machine on GCP, well-structured Docker Compose, a safety swap file, and Makefile automation will give you the performance of a giant at a fraction of the cost. In software engineering, simplicity under control always wins.
Want to try the product running on that VM?
SynapseOne uses artificial intelligence to forecast inventory demand, compare suppliers, and suggest exactly what to buy and when.