Deploy apps with git push to any cloud. AWS, GCP, Azure, or on-premises — same CLI, same workflow.
git push platform main # that's it
git clone https://github.com/ajaykumar127/heroku-clone.git
cd heroku-clone
make deploy-awsThat's it. The script checks your prerequisites, builds the binaries, starts the Control Plane, provisions an EKS cluster with Terraform, and waits for the Runtime Agent to register. When it finishes (~15 min) you'll see exactly what to do next.
Options:
# Different region or instance type
PLATFORM_AWS_REGION=eu-west-1 PLATFORM_NODE_TYPE=t3.large make deploy-aws
# Tear everything down
make destroy-awsPrerequisites (install before running):
go,terraform,awsCLI (configured withaws configure),kubectlAll installable viabrew install go terraform awscli kubectl
Platform is an open-source Heroku-style PaaS you host yourself. It separates into two planes:
- Control Plane — runs once, anywhere. Manages apps, auth, config, and job dispatch.
- Runtime Plane — one per cloud/region. Handles builds, deployments, and traffic. Deployable to AWS EKS, GCP GKE, or Azure AKS with the included Terraform.
Developers use one CLI and one API regardless of which cloud their apps run on.
Developer Tools (CLI / git push / Dashboard)
│
┌───────▼────────────┐
│ Control Plane │ ← runs once, anywhere
│ API · Auth · Jobs │
└───────┬────────────┘
│ HTTP polling (no inbound required)
┌────────┼────────────────┐
│ │ │
┌──▼──┐ ┌──▼──┐ ┌──────────▼───┐
│ AWS │ │ GCP │ │ Azure │
│ EKS │ │ GKE │ │ AKS │
│ ECR │ │ GAR │ │ ACR │
└─────┘ └─────┘ └──────────────┘
Runtime Agent + Builder + Deployer on each cluster
heroku-clone/
├── control-plane/ # Control Plane API server (Go)
│ ├── internal/
│ │ └── scheduler/ # Job dispatch — picks which runtime handles each deploy
│ └── migrations/ # SQL migrations (001–004)
├── runtime-plane/
│ ├── agent/ # Runtime Agent — registers, polls jobs, drives pipeline
│ ├── builder/ # Builder service — CNB/pack integration
│ └── deployer/ # Deployer service — kubectl apply
├── cli/ # platform CLI (Go + Cobra)
├── infra/
│ ├── aws/ # Terraform: EKS + ECR + VPC + IAM
│ ├── gcp/ # Terraform: GKE + Artifact Registry + VPC
│ └── azure/ # Terraform: AKS + ACR + VNet
├── poc/ # Original proof-of-concept (local dev)
├── api-design/ # API schema and DB schema docs
└── docs/ # Architecture, implementation guide
The
make deploy-awscommand above runs all of these automatically. Read this section if you want to understand what it does or run steps individually.
This guide takes you from zero to a working platform on AWS EKS in about 30 minutes. You will end up with:
- A running Control Plane (locally or on any server)
- An EKS cluster running the Runtime Agent
- The
platformCLI configured and working - A deployed sample app accessible via HTTP
Install these tools before starting:
| Tool | Version | Install |
|---|---|---|
| Go | 1.21+ | brew install go |
| Terraform | 1.5+ | brew install terraform |
| AWS CLI | 2.x | brew install awscli |
| kubectl | 1.28+ | brew install kubectl |
| git | any | pre-installed on macOS |
| pack CLI | 0.33+ | brew install buildpacks/tap/pack |
Configure your AWS credentials:
aws configure
# Enter your AWS Access Key ID, Secret, region (e.g. us-east-1), output format (json)Verify access:
aws sts get-caller-identity
# Should return your AWS account ID and IAM user/role ARNgit clone https://github.com/ajaykumar127/heroku-clone.git
cd heroku-clone
# Build the CLI
cd cli
go build -o bin/platform .
# Add to your PATH (or use ./bin/platform throughout this guide)
export PATH="$PWD/bin:$PATH"
cd ..
# Verify
platform --helpThe Control Plane is a single Go binary backed by SQLite (for local dev) or PostgreSQL (for production).
cd control-plane
# Build
go build -o bin/control-plane .
# Start (SQLite mode — no database setup needed)
DATABASE_URL=./platform.db \
PORT=8080 \
GIT_SERVER_HOST=localhost \
GIT_SERVER_PORT=2222 \
./bin/control-planeYou should see:
Starting Control Plane API
Environment: development
Database: ./platform.db
Port: 8080
✓ Database connected
✓ Auth schema ready
✓ Config vars schema ready
API Server listening on :8080
Leave this running in a terminal tab. In a new tab, verify it:
curl http://localhost:8080/health
# {"status":"ok"}For production, run the Control Plane on a server with a public IP and use PostgreSQL. Set
DATABASE_URL=postgres://user:pass@host:5432/platforminstead of the SQLite path.
cd cli
platform auth login --api-url http://localhost:8080Output:
Logged in. Token saved to ~/.platform/config
Token: pltf_abc123...
Your token is now saved. Every subsequent platform command reads it automatically from ~/.platform/config.
This creates an EKS cluster, ECR registry, VPC, and IAM roles — and deploys the Runtime Agent into the cluster automatically.
cd infra/aws
# Initialise Terraform providers
terraform init
# Preview what will be created (no changes yet)
terraform plan \
-var="control_plane_url=http://<YOUR_MACHINE_IP>:8080" \
-var="region=us-east-1" \
-var="cluster_name=platform-runtime"Replace
<YOUR_MACHINE_IP>with the IP address the EKS cluster can reach your Control Plane on. If running the Control Plane on the same machine, use your machine's LAN IP (not 127.0.0.1).
When the plan looks good, apply:
terraform apply \
-var="control_plane_url=http://<YOUR_MACHINE_IP>:8080" \
-var="region=us-east-1" \
-var="cluster_name=platform-runtime"Type yes when prompted. This takes 10–15 minutes. When complete you will see:
Apply complete! Resources: 42 added.
Outputs:
cluster_endpoint = "https://XXXXX.gr7.us-east-1.eks.amazonaws.com"
cluster_name = "platform-runtime"
ecr_repository_url = "123456789.dkr.ecr.us-east-1.amazonaws.com/platform-apps"
kubeconfig_command = "aws eks update-kubeconfig --name platform-runtime --region us-east-1"
Configure kubectl to talk to your new cluster:
aws eks update-kubeconfig --name platform-runtime --region us-east-1
# Verify the agent is running
kubectl get pods -n platform-system
# NAME READY STATUS RESTARTS
# runtime-agent-7d9f8b5c4-xk2p9 1/1 Running 0Switch back to your CLI:
platform runtime list --api-url http://localhost:8080Output:
NAME CLOUD REGION STATUS LAST SEEN
aws-us-east-1 aws us-east-1 active 12 seconds ago
The Runtime Agent running in EKS registered itself with the Control Plane on startup and is sending heartbeats every 30 seconds.
The Git server accepts git push from developers and triggers builds. Run it in a new terminal tab:
cd poc/git-server
go build -o bin/git-server .
API_URL=http://localhost:8080 ./bin/git-serverOutput:
Git server starting on :2222
Repos directory: ./repos
platform apps create my-first-app \
--api-url http://localhost:8080 \
--runtime aws-us-east-1Output:
Created app my-first-app
Git URL: ssh://git@localhost:2222/my-first-app.git
Web URL: http://my-first-app.localhost
Add the remote:
git remote add platform ssh://git@localhost:2222/my-first-app.git
Deploy:
git push platform main
Config vars are injected as environment variables into your app at deploy time:
platform config set my-first-app \
NODE_ENV=production \
DATABASE_URL=postgres://prod-host/mydb \
--api-url http://localhost:8080
# Verify
platform config get my-first-app --api-url http://localhost:8080
# DATABASE_URL: postgres://prod-host/mydb
# NODE_ENV: productionUse the included example Node.js app, or use your own.
cd poc/example-app
# Initialise a git repo if it isn't one already
git init
git add .
git commit -m "Initial commit"
# Point it at the platform Git server
git remote add platform ssh://git@localhost:2222/my-first-app.git
# Deploy
git push platform mainYou will see build output in your terminal:
-----> Receiving push for my-first-app
abc1234 -> def5678 (refs/heads/main)
-----> Triggering build for commit def5678
-----> Build queued successfully
-----> Deploy in progress...
In a new terminal, stream the build logs in real time:
# First get the release ID
platform releases list my-first-app --api-url http://localhost:8080
# VERSION COMMIT STATUS CREATED
# v1 def5678 building 2026-05-19 18:00:00
# Stream logs (replace <release-id> with the ID from above)
platform logs my-first-app <release-id> \
--tail \
--api-url http://localhost:8080Output while building:
=====> Building application: my-first-app
Commit: def5678
-----> Cloning repository
-----> Detecting application type
Detected: Node.js
-----> Building with buildpacks
...
=====> Build complete!
Image: 123456789.dkr.ecr.us-east-1.amazonaws.com/platform-apps/my-first-app:def5678
=====> Deploying to Kubernetes (aws-us-east-1)
-----> Applying manifests
-----> Waiting for rollout...
deployment "my-first-app" successfully rolled out
==> Build succeeded
Your app is now running on EKS and accessible via the Nginx Ingress. By default it is available at:
http://my-first-app.localhost
For a production domain, update your DNS to point *.yourdomain.com at the Nginx Ingress load balancer IP, then set:
terraform apply \
-var="apps_domain=yourdomain.com" \
...# Push a code change — a new release is created automatically
git commit -am "Fix bug" && git push platform main
# Check release history
platform releases list my-first-app --api-url http://localhost:8080
# VERSION COMMIT STATUS CREATED
# v2 a1b2c3 succeeded 2026-05-19 18:05:00
# v1 def5678 succeeded 2026-05-19 18:00:00
# Update a config var (triggers redeploy)
platform config set my-first-app LOG_LEVEL=debug --api-url http://localhost:8080# Authentication
platform auth login # Create a token and save it locally
platform auth tokens # List all tokens
# Apps
platform apps list # List all apps
platform apps create <name> # Create a new app
platform apps create <name> --runtime aws-us-east-1 # Create on a specific runtime
platform apps destroy <name> --confirm # Delete an app
# Deployments & releases
platform releases list <app> # List all releases
platform logs <app> <release-id> # View build logs (snapshot)
platform logs <app> <release-id> --tail # Stream build logs live
# Config vars
platform config get <app> # Show all config vars
platform config set <app> KEY=VALUE ... # Set one or more vars
platform config unset <app> KEY ... # Remove vars
# Runtime planes
platform runtime list # List registered runtime planes
platform runtime register # Manually register a runtime
--name aws-us-east-1
--cloud aws
--region us-east-1
--agent-url http://agent-host:9090Global flags available on every command:
--api-url http://localhost:8080 # Control Plane URL
--token pltf_abc123... # API token (or set PLATFORM_TOKEN env var)Once your Control Plane is running, adding a second cloud is just a terraform apply:
cd infra/gcp
terraform init
terraform apply \
-var="project_id=my-gcp-project" \
-var="control_plane_url=http://<YOUR_CONTROL_PLANE>:8080" \
-var="region=us-central1"
platform runtime list
# NAME CLOUD REGION STATUS
# aws-us-east-1 aws us-east-1 active
# gcp-us-central1 gcp us-central1 active ← newcd infra/azure
terraform init
terraform apply \
-var="control_plane_url=http://<YOUR_CONTROL_PLANE>:8080" \
-var="location=East US"
platform runtime list
# NAME CLOUD REGION STATUS
# aws-us-east-1 aws us-east-1 active
# gcp-us-central1 gcp us-central1 active
# azure-eastus azure eastus active ← newNow you can deploy to any cloud:
platform apps create my-app --runtime gcp-us-central1
platform apps create another-app --runtime azure-eastusFor production, run the Control Plane on a server (EC2, GCE, or any VPS) with PostgreSQL:
# On your server
export DATABASE_URL=postgres://platform:secret@localhost:5432/platform
export PORT=8080
export ENVIRONMENT=production
export GIT_SERVER_HOST=git.yourdomain.com
export GIT_SERVER_PORT=2222
./control-planeApply the DB migrations first:
psql $DATABASE_URL -f control-plane/migrations/001_initial_schema.sql
psql $DATABASE_URL -f control-plane/migrations/002_runtimes.sql
psql $DATABASE_URL -f control-plane/migrations/003_jobs.sql
psql $DATABASE_URL -f control-plane/migrations/004_app_runtime_fk.sqlThen point your Terraform at the public URL:
terraform apply -var="control_plane_url=https://platform.yourdomain.com" ...Runtime not showing up after terraform apply
Check the agent logs in the cluster:
kubectl logs -n platform-system -l app=runtime-agent --tail=50Common cause: the Control Plane URL is not reachable from inside the cluster. Make sure you used your machine's public/LAN IP, not localhost.
Build stuck in building status
Check the agent is polling:
kubectl logs -n platform-system -l app=runtime-agent -fIf the agent shows errors, the Control Plane may have restarted and the agent needs to re-register. Restart the agent pod:
kubectl rollout restart deployment/runtime-agent -n platform-systempack CLI not found during build
The builder falls back to builder.sh automatically. To get full CNB support, ensure pack is installed in the runtime agent image or set BUILDER_SCRIPT to point to a working script.
git push rejected with permission denied
The Git server accepts all SSH keys for the POC. If you see permission errors, ensure the SSH key is being sent:
ssh -p 2222 -v git@localhost| Layer | Technology |
|---|---|
| Control Plane | Go — net/http, gorilla/mux, SQLite / PostgreSQL |
| Runtime Agent | Go — stdlib only, polls over HTTP |
| Builder | Go + Cloud Native Buildpacks (pack CLI) |
| Deployer | Go + kubectl |
| Orchestration | Kubernetes 1.28+ |
| Container Registry | ECR (AWS) / Artifact Registry (GCP) / ACR (Azure) |
| Ingress | Nginx Ingress Controller |
| Infrastructure | Terraform 1.5+ |
| CLI | Go + Cobra |
- Architecture — Control Plane / Runtime Plane deep dive
- API Schema — Full REST API specification
- Database Schema — PostgreSQL schema
- Implementation Guide — How each component is built
- Infra README — Terraform usage for all three clouds
- POC README — Local proof-of-concept setup
Reference design and educational resource. Adapt freely.