Self-host Ploy: your own open-source Vercel alternative
How to self-host Ploy, the open-source Vercel alternative: one Docker command for a test instance, a split Docker Compose setup for production, GitHub App setup, and what you get for free, including databases, queues, workflows, cron, auth, and an AI gateway.

Managed deployment platforms are convenient right up to the moment you need your code, environment variables, and databases to stay on infrastructure you control, or the bill stops matching the traffic. At that point the choice is usually "build an internal platform" or "accept it".
Ploy is a third option: an open-source (AGPLv3), self-hostable serverless cloud. Run it on your own servers and you get push-to-deploy from GitHub, a preview URL per branch, serverless workers, SQLite databases, queues, durable workflows, cron, sandboxes, managed auth, and an AI gateway, for free, with no usage limits. This post is a practical guide to self-hosting Ploy as your own Vercel alternative.
What you need
- Docker and Docker Compose
- A GitHub OAuth App (sign-in) and a GitHub App (repository access and webhooks)
- A domain, recommended for anything beyond a test
- 2 GB of RAM to try it, 4 GB or more for production, and 10 GB or more of disk for deployments
Fastest path: one container
The unified image bundles every service for testing and small installs:
docker run -d \--name ploy \--restart unless-stopped \-p 3002:3002 -p 3005:3005 -p 4001:4001 -p 4002:4002 \-v ~/ploy_data:/var/lib/postgresql/data \-v ~/ploy_fs:/app/fs \-e AUTH_SECRET=your-secret-key-here \-e GITHUB_CLIENT_ID=your-github-client-id \-e GITHUB_CLIENT_SECRET=your-github-client-secret \-e GITHUB_WEBHOOK_SECRET=your-webhook-secret \ghcr.io/meetploy/ploy-unified:latest
docker run -d \--name ploy \--restart unless-stopped \-p 3002:3002 -p 3005:3005 -p 4001:4001 -p 4002:4002 \-v ~/ploy_data:/var/lib/postgresql/data \-v ~/ploy_fs:/app/fs \-e AUTH_SECRET=your-secret-key-here \-e GITHUB_CLIENT_ID=your-github-client-id \-e GITHUB_CLIENT_SECRET=your-github-client-secret \-e GITHUB_WEBHOOK_SECRET=your-webhook-secret \ghcr.io/meetploy/ploy-unified:latest
The dashboard is on :3002, the API on :4002, docs on :3005, and the handler that serves deployed apps on :4001. Pin latest to a release tag before relying on it.
There is also an install script that detects your Linux distribution, installs Docker if needed, writes a sample .env, and starts the services:
curl -fsSL https://raw.githubusercontent.com/meetploy/ploy/main/scripts/install.sh | sudo bash -s -- simple
curl -fsSL https://raw.githubusercontent.com/meetploy/ploy/main/scripts/install.sh | sudo bash -s -- simple
Use prod instead of simple for the split layout below.
Production path: split services
For production, run each service in its own container:
git clone https://github.com/meetploy/ploy.gitcd ploycp .env.example .envdocker compose -f infra/docker-compose.split.yml up -d
git clone https://github.com/meetploy/ploy.gitcd ploycp .env.example .envdocker compose -f infra/docker-compose.split.yml up -d
The split layout runs Postgres, Redis, the API, the dashboard, the builder, the handler, and the docs as separate containers. Only the API, dashboard, and handler should accept traffic from outside the service network; the internal API, database service, broker, and sandbox control plane stay private, which the bundled Compose files do by not publishing their ports. On Kubernetes or another orchestrator, enforce the same boundary with network policies.
GitHub setup
Two GitHub registrations, both created from your GitHub settings:
- OAuth App for signing in to the dashboard. Callback URL:
https://ploy.yourdomain.com/api/auth/callback/github. - GitHub App for reading repositories and receiving pushes. Webhook URL:
https://api.yourdomain.com/webhooks/github, with Contents and Metadata read permissions, Webhooks read and write, and the Push and Repository events.
Put the client id and secret, app id, private key, and webhook secret in .env:
AUTH_SECRET=... # openssl rand -hex 32BETTER_AUTH_URL=https://ploy.yourdomain.comGITHUB_CLIENT_ID=...GITHUB_CLIENT_SECRET=...GITHUB_APP_ID=...GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"GITHUB_WEBHOOK_SECRET=... # openssl rand -hex 32NEXT_PUBLIC_API_URL=https://api.yourdomain.com
AUTH_SECRET=... # openssl rand -hex 32BETTER_AUTH_URL=https://ploy.yourdomain.comGITHUB_CLIENT_ID=...GITHUB_CLIENT_SECRET=...GITHUB_APP_ID=...GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"GITHUB_WEBHOOK_SECRET=... # openssl rand -hex 32NEXT_PUBLIC_API_URL=https://api.yourdomain.com
If you do not want GitHub in the loop at all, projects can deploy from Ploy Git, a repository hosted by your Ploy instance that accepts plain git push.
TLS and domains
Put a reverse proxy in front: the dashboard on ploy.yourdomain.com, the API on api.yourdomain.com, and a wildcard such as *.ploy-deployments.yourdomain.com pointed at the handler so every deployment and preview gets its own hostname. A Caddy configuration is in the self-host guide. Keep the proxy's idle upstream keep-alive under 5 seconds (keepalive 3s in Caddy), and set PUBLIC_PROTOCOL=https so workers see the right scheme in request.url.
Storage choices
- Project databases default to
DB_STORAGE_MODE=basic, one SQLite file per project underDB_STORAGE_DIR. SetDB_STORAGE_MODE=turso-cloudto place them in Turso Cloud for managed replication and point-in-time restore. - Sandboxes default to
SANDBOX_PROVIDER=docker, running on the sandbox host with per-project network isolation.SANDBOX_PROVIDER=boatuses the hosted Boat provider instead, with no Docker daemon needed. - Worker CPU limits are configurable per plan with
WORKER_CPU_LIMIT_MS_*variables; organizations on a self-hosted instance use the Free limits unless you raise them.
What self-hosting gets you
| On hosted Ploy | Self-hosted |
|---|---|
| Free plan: 3 repositories, 1 concurrent | No usage limits |
| Pro: $10/month | $0, your infrastructure |
| Data on Ploy's infrastructure | Code, env vars, and databases stay with you |
| Ploy operates it | You operate it: updates, backups, monitoring |
The developer experience is identical: the same ploy.yaml, the same @meetploy/* packages, the same ploy dev emulator, and the same dashboard.
Keeping it running
docker compose -f infra/docker-compose.split.yml logs -f # logsdocker exec ploy-postgres pg_dump -U postgres ploy > backup.sql # backupgit pull origin main && docker compose pull && docker compose up -d # update
docker compose -f infra/docker-compose.split.yml logs -f # logsdocker exec ploy-postgres pg_dump -U postgres ploy > backup.sql # backupgit pull origin main && docker compose pull && docker compose up -d # update
Set LOG_LEVEL=debug in .env when something misbehaves, and check the builder and handler logs first for failed deployments.
Frequently Asked Questions
Is self-hosted Ploy feature-complete?
Yes. Databases, queues, workflows, cron, sandboxes, auth, and the AI gateway are part of the open-source platform. Only the ee/ directory is under a commercial license.
Can I run it on a single small server?
Yes. The unified container runs on 2 GB of RAM for testing. Use the split Compose file and more memory for production workloads.
Does it support Next.js, Vite, and TanStack Start?
Yes, the same frameworks as hosted Ploy, with the same ploy.yaml configuration. See Deploy Next.js on Ploy.
What license is Ploy under?
AGPLv3, with the ee/ directory under a separate commercial license. See the LICENSE file for the exact terms.
Get started
- Read the self-host guide or the quickstart
- Prefer not to run it yourself? Try hosted Ploy free
- Next: Ploy vs Vercel