Want to test your changes in a live environment? Deploy a preview via manual workflow dispatch.
Each preview environment includes:
- ✅ Isolated full-stack environment (Postgres + Backend + Frontend)
- ✅ Clean path-based URLs via NGINX routing
- ✅ Live database with migrations applied
- ✅ Access via Tailscale VPN
- ✅ Automatic cleanup when PR closes
- Open a PR in either
refactor-platform-rsorrefactor-platform-fe - Go to Actions → "Deploy PR Preview (Manual Select)" → Run workflow
- Choose commits from the dropdowns (includes latest
main+ HEAD of every open PR from both repos) - For any commit not in the dropdown, paste the exact SHA in the override fields
- Wait for deployment (~5-10 minutes for first build)
- Check PR comment for your unique URLs
- Connect to Tailscale VPN (required for access)
Note: Dropdowns auto-refresh on every merge to
mainand on every PR update.
Example PR Comment:
🚀 PR Preview Environment Deployed!
Frontend: http://neo.rove-barbel.ts.net/pr-201/
Backend API: http://neo.rove-barbel.ts.net/pr-201/api/
Health Check: http://neo.rove-barbel.ts.net/pr-201/health
Base Path: /pr-201/
Access Method: NGINX path-based routing (no direct port access)Each PR gets a unique URL path based on the PR number:
| Service | URL Pattern | Example (PR #201) |
|---|---|---|
| Frontend | /pr-<NUM>/ |
http://neo.rove-barbel.ts.net/pr-201/ |
| Backend API | /pr-<NUM>/api/ |
http://neo.rove-barbel.ts.net/pr-201/api/ |
| Health Check | /pr-<NUM>/health |
http://neo.rove-barbel.ts.net/pr-201/health |
How NGINX Routes Traffic:
- All requests go through NGINX on port 80
- NGINX uses regex to match
/pr-<NUM>/paths - Routes to correct Docker containers based on PR number
- No application ports exposed to host
Backend PR:
- User triggers "Deploy PR Preview (Manual Select)" from backend repo Actions tab
- Backend: Builds from selected commit 📦
- Frontend: Builds from selected commit (or uses main-arm64 if main commit selected)
- Deploys: Full stack with your chosen commit combination
Frontend PR:
- User triggers "Deploy PR Preview (Manual Select)" from frontend repo Actions tab
- Frontend: Builds from selected commit 📦
- Backend: Builds from selected commit (or uses main-arm64 if main commit selected)
- Deploys: Full stack with your chosen commit combination
┌──────────────────────────────────────────────┐
│ Developer (via Tailscale VPN) │
└────────────────┬─────────────────────────────┘
│ HTTP
↓
┌──────────────────────────────────────────────┐
│ NGINX (neo.rove-barbel.ts.net:80) │
│ ├─ /pr-201/ → pr-201-frontend:3000 │
│ ├─ /pr-201/api/ → pr-201-backend:3000 │
│ └─ /pr-202/ → pr-202-frontend:3000 │
└────────────────┬─────────────────────────────┘
│ Docker Network: preview-ingress
↓
┌──────────────────────────────────────────────┐
│ Docker Containers (No Host Ports) │
│ ┌──────────────────────────────────────┐ │
│ │ PR-201 Environment │ │
│ │ ├─ pr-201-frontend-1 (3000) │ │
│ │ ├─ pr-201-backend-1 (3000) │ │
│ │ └─ pr-201-postgres-1 (5432, internal)│ │
│ └──────────────────────────────────────┘ │
│ ┌──────────────────────────────────────┐ │
│ │ PR-202 Environment │ │
│ │ ├─ pr-202-frontend-1 (3000) │ │
│ │ ├─ pr-202-backend-1 (3000) │ │
│ │ └─ pr-202-postgres-1 (5432, internal)│ │
│ └──────────────────────────────────────┘ │
└──────────────────────────────────────────────┘Security Features:
- ✅ Single ingress point (NGINX only)
- ✅ No direct container port access
- ✅ Postgres never exposed externally
- ✅ Network isolation between PRs
All secrets are managed in ONE place: Backend repo's pr-preview environment.
This means:
- ✅ Frontend repo needs zero PR preview secrets
- ✅ No secret duplication across repos
- ✅ Single source of truth for configuration
Backend pr-preview Environment Contains:
- RPi5 SSH connection details
- Database credentials
- TipTap API keys
- Resend API keys
- Frontend build configuration
Backend Repository:
.github/workflows/ci-deploy-pr-preview.yml- Reusable workflow (does the heavy lifting).github/workflows/dispatch-pr-preview.yml- Manual dispatch for backend PRs.github/workflows/cleanup-pr-preview-backend.yml- Cleanup on PR close.github/workflows/cleanup-pr-preview.yml- Reusable cleanup workflow.github/workflows/refresh-preview-commits.yml- Updates commit dropdown choices
Frontend Repository:
.github/workflows/dispatch-pr-preview-frontend.yml- Manual dispatch for frontend PRs (calls backend reusable workflow).github/workflows/cleanup-pr-preview-frontend.yml- Cleanup on PR close
Static Configuration:
/etc/nginx/sites-enabled/pr-previews.conf- Single config handles all PRs- Uses regex to dynamically route based on PR number
- No per-PR config generation needed
- Automatic container discovery via Docker DNS
# Check PR #201 health
curl http://neo.rove-barbel.ts.net/pr-201/health
# Expected response
PR #201 routing active# List users endpoint (PR #201)
curl http://neo.rove-barbel.ts.net/pr-201/api/v1/users
# Create a test user
curl -X POST http://neo.rove-barbel.ts.net/pr-201/api/v1/users \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","name":"Test User"}'Visit http://neo.rove-barbel.ts.net/pr-201/ in your browser (Tailscale required).
Connect to your PR's database via SSH tunnel:
# SSH into Neo
ssh user@neo.rove-barbel.ts.net
# Access postgres container directly
docker exec -it pr-201-postgres-1 \
psql -U refactor -d refactor_platform
# Or create SSH tunnel
ssh -L 5432:localhost:5432 user@neo.rove-barbel.ts.net
# Then connect from tunnel (note: postgres not exposed on host)
# You'll need to use docker exec approach above-
Check workflow logs:
- Go to PR → "Checks" tab → Click on failed workflow
- Review error messages in logs
-
Common issues:
- Linting errors: Fix code formatting issues
- Test failures: Ensure all tests pass locally first
- Build errors: Check Dockerfile and dependencies
- Migration errors: Verify database migrations are valid
- Image pull errors: Check GHCR permissions and image exists
-
Verify Tailscale connection:
tailscale status # Should show you're connected to the network -
Check NGINX routing:
# Test NGINX health endpoint curl http://neo.rove-barbel.ts.net/health # Test PR-specific health curl http://neo.rove-barbel.ts.net/pr-201/health
-
Verify containers running:
ssh user@neo.rove-barbel.ts.net docker ps --filter 'name=pr-201' -
Check container logs:
ssh user@neo.rove-barbel.ts.net docker logs pr-201-backend-1 --tail 50 docker logs pr-201-frontend-1 --tail 50
- Re-run dispatch: Go to Actions → "Deploy PR Preview (Manual Select)" → Run workflow with updated commits
- Re-run workflow: Go to Actions → Re-run failed jobs
- Verify build: Check that new image was built and pushed
502 Bad Gateway:
- Container not running or name mismatch
- Check:
docker ps --filter 'name=pr-<NUM>' - Verify container names match pattern:
pr-<NUM>-frontend-1,pr-<NUM>-backend-1
404 Not Found:
- Incorrect path in URL
- Ensure path starts with
/pr-<NUM>/ - Check NGINX config:
cat /etc/nginx/sites-enabled/pr-previews.conf
Preview environments are automatically cleaned up when:
- PR is closed
- PR is merged
The cleanup workflow removes:
- Docker containers
- Database volumes
- Temporary files
- Compose and environment files
Note: NGINX config is static and shared across all PRs, so it's not removed during cleanup.
If you need to manually clean up a preview:
# SSH into Neo
ssh user@neo.rove-barbel.ts.net
# Stop and remove PR environment
docker compose -p pr-201 down -v
# Remove compose and env files
rm ~/pr-201-compose.yaml ~/pr-201.env
# Verify cleanup
docker ps --filter 'name=pr-201'
# Should show no containersThe manual dispatch workflow lets you pick any combination of backend and frontend commits:
- Go to Actions → "Deploy PR Preview (Manual Select)" → Run workflow
- Select backend commit from dropdown (or paste exact SHA in override field)
- Select frontend commit from dropdown (or paste exact SHA in override field)
- The workflow validates both commits exist before deploying
# SSH into Neo
ssh user@neo.rove-barbel.ts.net
# View NGINX config
cat /etc/nginx/sites-enabled/pr-previews.conf
# Test NGINX configuration syntax
sudo nginx -t
# View NGINX access logs
sudo tail -f /var/log/nginx/access.log | grep 'pr-'
# View NGINX error logs
sudo tail -f /var/log/nginx/error.log | grep 'pr-'Real-time logs during deployment:
# SSH into Neo
ssh user@neo.rove-barbel.ts.net
# View backend logs
docker logs pr-201-backend-1 -f
# View frontend logs
docker logs pr-201-frontend-1 -f
# View postgres logs
docker logs pr-201-postgres-1 -f
# View migration logs
docker logs pr-201-migrator-1# SSH into Neo
ssh user@neo.rove-barbel.ts.net
# List all containers for your PR
docker compose -p pr-201 ps
# View resource usage
docker stats pr-201-backend-1 pr-201-frontend-1 pr-201-postgres-1
# Check container networks
docker inspect pr-201-backend-1 --format='{{range $k := .NetworkSettings.Networks}}{{printf "%s\n" $k}}{{end}}'
# Expected: pr-201_default, preview-ingress# Check preview-ingress network
docker network inspect preview-ingress
# Verify NGINX can reach containers
docker run --rm --network preview-ingress alpine ping -c 1 pr-201-backend-1
# Should succeed
# Verify postgres is NOT on preview-ingress (security)
docker run --rm --network preview-ingress alpine ping -c 1 pr-201-postgres-1
# Should fail (postgres only on pr-201_default network)- Tailscale VPN Required: Previews are not publicly accessible
- NGINX Single Ingress: All traffic goes through NGINX (port 80 only)
- No Direct Port Access: Application containers don't expose host ports
- Postgres Isolation: Database never accessible from preview-ingress network
- Network Isolation: Each PR has isolated Docker network for internal communication
- Shared Environment: All PRs deploy to same Neo server (isolated by Docker networks)
- Temporary Data: Database resets when environment is cleaned up
- Do Not: Store sensitive production data in preview environments
Want to improve the PR preview system?
Key files to modify:
ci-deploy-pr-preview.yml- Main deployment logic (reusable workflow)docker-compose.pr-preview.yaml- Service definitions and network configurationdispatch-pr-preview.yml/dispatch-pr-preview-frontend.yml- Manual dispatch triggersnginx/conf.d/pr-previews.conf- NGINX routing configuration (static, handles all PRs)
After changes:
- Test in a PR first
- Document changes in this runbook
- Update PR template if user-facing changes
NGINX Configuration Changes:
- The NGINX config is static and must be manually updated on Neo if modified
- Location:
/etc/nginx/sites-enabled/pr-previews.conf - After updating:
sudo nginx -t && sudo nginx -s reload
- GitHub Actions Workflow Syntax
- Docker Compose Documentation
- Docker Networking
- NGINX Configuration
- Tailscale Setup Guide
Questions? Ask in Levi in Slack or open an issue.