Reviewed-on: #5
PR Previews (PP)
PR Previews is a self-hosted preview environment service for Gitea pull requests.
When someone opens or updates a pull request, PP starts an EC2 instance in your AWS account, checks out the PR branch, builds the project, runs it, and posts the live preview URL back to the PR. Reviewers can open the running app without cloning the branch locally.
PP is built for Gitea only. It does not support GitHub or GitLab.
Get Started
The fastest way to try PP is with Docker Compose:
cp example.env .env
Edit .env and set at least:
SESSION_SECRET=<long random string>
PP_BASE_URL=https://your-public-pp-url.example.com
ENCRYPTION_KEY=<64 hex chars>
Generate ENCRYPTION_KEY with:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Then start PP:
docker compose up -d
Open http://localhost:5000, create the first user, and follow the setup wizard to connect Gitea, AWS, and your first repo.
What It Does
- Watches Gitea pull requests through webhooks.
- Creates one preview environment per PR.
- Reuses the same EC2 instance when new commits are pushed.
- Streams deploy logs into the web UI.
- Edits a single PR comment with the current preview status and URL.
- Stops old previews manually, on PR close, or after inactivity.
- Lets users configure preinstalled runtimes/tools, build commands, run commands, env vars, ports, instance type, and Docker Compose usage per repo.
How It Works
- A Gitea webhook sends a pull request event to PP.
- PP verifies the webhook signature and queues a deploy job.
- The worker provisions or reuses an AWS EC2 instance.
- PP connects over SSH, clones the PR branch, installs dependencies, builds, and starts the app.
- PP posts or updates a Gitea PR comment with the preview URL.
Preview URLs look like:
http://<ec2-public-ip>:<configured-port>
Requirements
- A running Gitea instance.
- A Gitea personal access token for the account that should post preview comments.
- An AWS account with EC2 permissions.
- Docker and Docker Compose to run PP itself.
- A public
PP_BASE_URLthat Gitea can reach for webhooks.
For local-only testing, PP_BASE_URL still needs to be reachable by Gitea. Use a tunnel or a real public URL if your Gitea instance is not running on the same machine.
Quick Start
-
Copy the environment template:
cp example.env .env -
Edit
.env:SESSION_SECRET=<long random string> PP_BASE_URL=https://pp.example.com ENCRYPTION_KEY=<64 hex chars>Generate a valid encryption key with:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" -
Start PP:
docker compose up -d -
Open the UI:
http://localhost:5000 -
Create the first user. The first user becomes the founder admin.
-
Complete the setup wizard:
- Add your Gitea instance URL and PAT.
- Add your AWS credentials and region.
- Enable a repo.
- Configure how that repo should build and run.
Environment Variables
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
Yes | PostgreSQL connection string. In Docker Compose this is set automatically for the bundled database. |
SESSION_SECRET |
Yes | Secret used for session cookies. Use a long random value. |
PP_BASE_URL |
Yes | Public URL of this PP instance, with no trailing slash. Used for webhooks, CORS, and PR comment links. |
ENCRYPTION_KEY |
Yes | Exactly 64 hex characters. Encrypts Gitea PATs, AWS credentials, and SSH private keys at rest. |
PORT |
No | Backend port. Defaults to 5000. |
LOG_LEVEL |
No | trace, debug, info, warn, or error. Defaults to info. |
NODE_ENV |
No | development, production, or test. Defaults to development. |
Important: changing ENCRYPTION_KEY after setup makes existing encrypted credentials unreadable.
Gitea Setup
PP can register repo webhooks automatically when you enable a repo in the UI.
The Gitea PAT should belong to the account that should post PR comments. It needs permissions to:
- Read repositories.
- Read pull request and issue metadata.
- Write issue comments.
- Create, update, and delete repo webhooks.
PP registers webhooks named PR Previews with these events:
pull_requestpull_request_syncissue_comment
AWS Setup
PP launches preview instances in your AWS account. Each preview gets its own EC2 instance, SSH key pair, and security group.
Required IAM policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:RunInstances",
"ec2:TerminateInstances",
"ec2:DescribeInstances",
"ec2:CreateSecurityGroup",
"ec2:DeleteSecurityGroup",
"ec2:AuthorizeSecurityGroupIngress",
"ec2:DescribeSecurityGroups",
"ec2:CreateKeyPair",
"ec2:DeleteKeyPair",
"ec2:CreateTags",
"sts:GetCallerIdentity"
],
"Resource": "*"
}
]
}
Every EC2 instance created by PP is tagged with pp:managed=true plus user, repo, PR number, and preview ID metadata.
Repo Configuration
Each enabled repo has its own preview configuration:
- EC2 instance type.
- Inactivity timeout.
- Public app port.
- Environment variables.
- Apt packages.
- Setup commands.
- Build commands.
- Post-build commands.
- Run command.
- Preinstall options for Docker + Compose, Node.js/version, Python, Go, Lua, build tools, and custom apt packages.
- Docker Compose mode and compose file path.
- Disabled
/ppcommands. - Deny list of Gitea usernames to ignore.
Docker Compose mode runs:
docker compose -f <compose-file> up -d --build --force-recreate
Non-compose mode runs your configured commands and starts the app with your configured run command.
PR Commands
PP also listens for commands in PR comments. The first line must start with /pp .
| Command | Action |
|---|---|
/pp help |
Post the available command list. |
/pp rebuild |
Rebuild and restart the preview. If the preview is stopped, a new instance is provisioned. |
/pp stop |
Stop the preview and terminate its EC2 instance. |
/pp start |
Start a stopped preview, or unignore and deploy an ignored preview. |
/pp logs |
Post the last deploy log lines as a PR comment. |
/pp ignore |
Ignore future events for this PR until /pp start is used. |
Commands can be disabled per repo, except /pp help.
Operating Notes
- Webhooks return quickly. Deploy work is handled asynchronously by the job worker.
- A newer deploy for the same PR cancels the currently running deploy.
- Stopped previews release their EC2 instance, SSH key pair, and security group.
- Stopped and failed preview records are cleaned up after the configured retention period.
- On startup, PP looks for orphaned managed EC2 instances and terminates ones that no longer match an active preview.
- Preview logs are stored in PostgreSQL and capped by the admin log size setting.
- Live app logs are available from the preview detail page while a preview is running.
Security Model
- Gitea PATs, AWS credentials, and SSH private keys are encrypted at rest.
- Webhook payloads are verified with HMAC-SHA256 before processing.
- API responses do not return raw stored secrets.
- EC2 SSH keys are generated per preview launch and deleted on stop.
- Users can manage only their own repos and previews.
- Admins can manage users, global settings, and all previews.
Keep ENCRYPTION_KEY backed up. Losing it means PP cannot decrypt saved credentials.
Development
This is a pnpm workspace monorepo:
backend/ Node.js API and workers
frontend/ React, Vite, and Tailwind UI
prisma/ Prisma schema and migrations
Use pnpm, not npm or yarn.
Start only the database:
docker compose up -d pp-db
Apply migrations:
cd backend
pnpm migrate
Run the backend on port 5000:
cd backend
pnpm install
pnpm dev
Run the frontend dev server on port 3000:
cd frontend
pnpm install
pnpm dev
Build backend:
cd backend
pnpm build
Build frontend:
cd frontend
pnpm build
Prisma schema lives at prisma/schema.prisma in the repo root. The backend package scripts already pass the correct schema path.
Production Deployment
The included Compose stack runs:
pp-db: PostgreSQL.pp-backend: the backend API, workers, migrations, and built frontend.
Start or update the stack with:
docker compose up -d
The container runs Prisma migrations on startup before starting the backend.
Project References
- Product behavior:
SPEC.md - Environment template:
example.env - Agent and contributor implementation notes:
AGENTS.md - Contributing guide:
CONTRIBUTING.md - Security policy:
SECURITY.md - License: Apache License 2.0 (
LICENSE)