This sort of thing should be commoditized at this point, right?
Spin up temporary environments for pull requests. Each PR gets its own isolated environment with a public URL.
ephemeral-demo-v4.mp4
Contents: How It Works | Quick Start | Integrating Your Project | Setup | Performance | Cost
GitHub PR Event → Cloudflare Worker → SQS → Lambda
├─ Launch EC2
├─ Clone repo and run docker-compose
├─ Create Cloudflare Quick Tunnel
└─ Post URL to GitHub PR
When you open a PR, the system:
- Receives the webhook and queues a deployment
- Launches an EC2 instance from a pre-built AMI
- Clones your branch and starts services with Docker Compose
- Creates a public tunnel URL
- Comments the URL on your PR
Environments stop after 4 hours idle and terminate after 24 hours stopped. A reconciler runs every 30 minutes to clean up orphaned instances from closed PRs.
# Copy and configure environment
cp .env.example .env
# Edit .env with your AWS, Cloudflare, and GitHub credentials
# Deploy everything
make deploy| Command | Purpose |
|---|---|
make deploy |
Full deployment (Terraform + AMI + Worker + Lambda) |
make destroy |
Tear down all infrastructure |
make tf-plan |
Preview infrastructure changes |
make tf-apply |
Apply infrastructure changes |
make ami-build |
Build the EC2 environment AMI |
make worker-dev |
Run Cloudflare Worker locally (port 8787) |
make worker-deploy |
Deploy Worker to Cloudflare |
make lambda-update |
Rebuild and deploy Lambda functions |
make test-e2e |
Run end-to-end tests |
| Component | Location | Role |
|---|---|---|
| Webhook Handler | worker/src/index.ts |
Receives GitHub webhooks, queues to SQS |
| Deploy Lambda | src/deployer/ |
Orchestrates EC2, tunnels, and GitHub updates |
| Cleanup Lambda | src/cleanup/ |
Auto-stops idle environments |
| Reconciler Lambda | src/reconciler/ |
Terminates orphans, reconciles state |
| AWS Infrastructure | infra/aws/ |
Terraform for EC2, Lambda, SQS, DynamoDB, VPC |
| AMI Builder | packer/ |
Amazon Linux 2023 with Docker and cloudflared |
This repo includes a Claude Code plugin that helps integrate any project with the ephemeral environment system.
/plugin marketplace add jack-michaud/ephemeral-environments
/plugin install ephemeral-environments@ephemeral-environmentsIn your project directory, run:
/integrateThe command analyzes your project and:
- Checks for a compatible
docker-compose.yml - Verifies port 80 is exposed
- Detects your tech stack and dependencies
- Generates configuration if needed
Your project needs:
- A
docker-compose.ymlthat exposes port 80 - A Dockerfile that builds your application
- The GitHub App installed on your repository
Example docker-compose.yml:
services:
app:
build: .
ports:
- "80:3000" # Expose your app on port 80The system uses a GitHub App to receive webhooks and post comments. Run the setup script:
./scripts/setup-github-app.shThis guides you through creating an app with minimal permissions:
- Contents: Read (clone repos)
- Commit statuses: Write (post status checks)
- Pull requests: Write (post comments)
After creating the app, save the private key as github-app.pem and add the App ID to your .env.
Setup scripts create credentials with minimal permissions:
./scripts/setup-aws-credentials.sh
./scripts/setup-cloudflare-token.shAll settings live in .env. See .env.example for the full list.
Typical end-to-end deployment times from webhook to environment ready:
| Phase | Duration | Description |
|---|---|---|
| Webhook → Lambda | ~1s | Cloudflare Worker queues to SQS |
| Secrets + Auth | ~2-3s | Fetch Cloudflare/GitHub credentials |
| EC2 Launch | ~1-2s | Request instance from launch template |
| Instance Ready | ~6-7s | Wait for instance to pass status checks |
| SSM Bootstrap | ~50-65s | Clone repo, docker-compose build/up, tunnel start |
| Total | ~60-75s | End-to-end deployment |
Rebuild times (existing environment): ~60-65s (terminates old instance first)
Cleanup times:
- Destroy on PR close: <1s
- Auto-stop (4h idle): Immediate stop
- Terminate (24h stopped): Immediate terminate
- Reconciler cleanup: Every 30 min scan
Metrics last validated: 2026-01-18 (from Lambda CloudWatch logs)
Idle cost: ~$2-3/month (no active environments)
| Resource | Monthly Cost | Notes |
|---|---|---|
| Secrets Manager (2x) | $0.80 | GitHub App + Cloudflare credentials |
| CloudWatch Logs | ~$1 | Scheduled Lambda execution logs |
| Lambda/EventBridge | <$0.50 | Free tier covers cleanup/reconciler runs |
Active environments: EC2 instances (t3.small ~$0.02/hour) + data transfer. Environments auto-stop after 4 hours idle and terminate after 24 hours stopped.
Design decisions that minimize cost:
- DynamoDB on-demand (scales to zero)
- Lambda pay-per-invocation (no provisioned concurrency)
- Public subnets with public IPs (no NAT Gateway)
- No VPC endpoints (EC2 reaches SSM over internet)
- TypeScript: Cloudflare Worker
- Python 3.11: Lambda functions (boto3, PyGithub)
- Terraform: AWS and Cloudflare infrastructure
- Packer: AMI builds
- Docker Compose: Application runtime