Building a self-hosted Vercel-style PaaS with Next.js 16 and AWS ECS Fargate
When I set out to build Nebula, my goal was ambitious: replicate the magical developer experience of Vercel or Heroku, but self-hosted on my own AWS infrastructure. I wanted a system where a developer could simply link a GitHub repository, and within minutes, have a live URL with zero-downtime rollouts.
This post breaks down the architecture of Nebula, the challenges of distributed systems, and how Next.js 16 and AWS ECS Fargate made it possible. (Check out the project on my homepage under Nebula PaaS).
The Architecture: Control Plane vs. Data Plane
Building a Platform-as-a-Service (PaaS) requires a strict separation of concerns. You can't have your API server handling heavy Docker builds, or your entire system will crash under load.
1. The Control Plane (Next.js 16 & PostgreSQL)
The brain of Nebula is the control plane, built with Next.js 16 using the App Router. It serves as both the user dashboard and the API gateway.
When a user logs in via GitHub OAuth, Nebula requests repository access. The Next.js backend uses Prisma and PostgreSQL to maintain state—storing user profiles, project metadata, environment variables, and deployment histories.
Next.js Server Actions proved incredibly useful here. Instead of building complex REST API endpoints for every button click (like "Trigger Deployment"), I could securely mutate database state directly from the React components.
However, handling environment variables required absolute security. Before any secret touches PostgreSQL, it is encrypted using AES-256-GCM. The decryption key never leaves the secure server environment, ensuring that even if the database is compromised, the secrets remain safe.
2. The Build Engine (Redis BullMQ & Docker)
When a deployment is triggered (either manually or via a GitHub Webhook), the control plane does not build the code. Instead, it pushes a job payload to a Redis queue managed by BullMQ.
A separate fleet of worker nodes (running Node.js) listens to this queue. When a worker picks up a job:
1. It clones the specific commit from GitHub.
2. It analyzes the repository structure to detect the framework (Next.js, Vite, etc.).
3. If no \Dockerfile\ is present, it injects a highly optimized, multi-stage Dockerfile based on the detected framework.
4. It builds the Docker image.
5. It pushes the compiled image to an AWS Elastic Container Registry (ECR).
One of the hardest parts of the build engine was streaming real-time logs back to the user. I built a custom WebSocket multiplexer that pipes \stdout\ from the Docker build process directly through Redis pub/sub and back to the Next.js client, giving the user that satisfying terminal output in the browser.
3. The Execution Layer (AWS ECS Fargate)
Once the image is safely in ECR, it's time to run it. I chose AWS ECS (Elastic Container Service) with Fargate for the execution layer. Fargate is serverless compute for containers—meaning I didn't have to manage underlying EC2 instances.
The Next.js control plane uses the AWS SDK to orchestrate the deployment:
- It creates a new ECS Task Definition pointing to the newly uploaded image tag.
- It updates the ECS Service to use this new Task Definition.
- ECS handles the rolling update, spinning up the new container and draining the old one only when the new one passes health checks (zero-downtime deployments).
Dynamic Routing and Custom Domains
The final puzzle piece was routing traffic. If a user deploys an app, how does \user-app.nebula.dev\ map to the correct ECS container?
I deployed a custom Nginx reverse proxy alongside an AWS Application Load Balancer (ALB). The control plane dynamically updates a routing table in Redis, mapping subdomains (and custom domains) to the internal IP addresses of the running ECS tasks. Nginx uses an OpenResty Lua script to query Redis on every request and proxy the traffic to the correct container.
Conclusion
Building Nebula was a masterclass in distributed systems. From handling WebSockets and Redis queues to orchestrating AWS infrastructure via code, the complexity was immense. But the result—a fully automated, self-hosted deployment pipeline—was entirely worth it.
If you are a full-stack developer looking to level up your systems design, I highly recommend building your own mini-PaaS.
Mahesh Chopade is a Full Stack Developer and AI Engineer. Explore more of my work on my homepage.