Deployment
Deployment
How apps get from code to production.
Deployment Model
Pvdify uses an external CI, image-based deploy model:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐│ GitHub │────▶│ GitHub │────▶│ GHCR │────▶│ Pvdify ││ Push │ │ Actions │ │ Image │ │ Deploy │└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ git push build + test push image pvdify deployWhy External CI?
| Constraint | Reason |
|---|---|
| No builds on VPS | CPU/memory reserved for running apps |
| Kernel 3.10 | Limited build tooling compatibility |
| Security | Build-time secrets stay in CI |
| Consistency | Same image runs everywhere |
Workflow: Standard Deploy
1. Push Code
git push origin main2. CI Builds Image (GitHub Actions)
name: Deployon: push: branches: [main]
jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Build and push uses: docker/build-push-action@v5 with: push: true tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
- name: Deploy to Pvdify run: | gh pvdify deploy --image ghcr.io/${{ github.repository }}:${{ github.sha }} env: PVDIFY_TOKEN: ${{ secrets.PVDIFY_TOKEN }}3. Deploy via CLI
# Triggered by CI or manuallypvdify deploy myapp --image ghcr.io/org/app:abc1234. Blue-Green Swap
┌─────────────────────────────────────────────────────┐│ Cloudflare Tunnel │└───────────────────────────┬─────────────────────────┘ │ ┌─────────────┴─────────────┐ ▼ ▼ ┌────────────┐ ┌────────────┐ │ Blue (v1) │ │ Green (v2) │ │ :3001 │ │ :3002 │ │ (active) │ ──swap──▶ │ (active) │ └────────────┘ └────────────┘- New container starts on alternate port
- Health check passes
- Tunnel route switches to new port
- Old container stops (after drain period)
Workflow: PR Previews
Automatic preview environments for pull requests:
GitHub Action
name: Previewon: pull_request: types: [opened, synchronize]
jobs: preview: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Build preview uses: docker/build-push-action@v5 with: push: true tags: ghcr.io/${{ github.repository }}:pr-${{ github.event.number }}
- name: Deploy preview run: | gh pvdify preview --pr ${{ github.event.number }} env: PVDIFY_TOKEN: ${{ secrets.PVDIFY_TOKEN }}
- name: Comment URL uses: actions/github-script@v7 with: script: | github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: '🚀 Preview deployed: https://myapp-pr-${{ github.event.number }}.pvdify.win' })Preview Lifecycle
| Event | Action |
|---|---|
| PR opened | Create preview slot |
| PR updated | Update preview |
| PR merged/closed | Delete preview (24h delay) |
Workflow: Rollback
Instant rollback to previous release:
# Rollback to previouspvdify rollback myapp
# Rollback to specific versionpvdify rollback myapp --version 41
# View release historypvdify releases myappRollback is instant because:
- Previous image is cached
- Previous config version is stored
- Just a port swap operation
Deploy Commands
CLI
# Deploy specific imagepvdify deploy myapp --image ghcr.io/org/app:v1.2.3
# Deploy with config changepvdify config:set DATABASE_URL=... && pvdify deploy myapp
# Deploy to stagingpvdify deploy myapp-staging --image ghcr.io/org/app:v1.2.3GitHub Extension
# Deploy current branchgh pvdify deploy
# Deploy specific imagegh pvdify deploy --image ghcr.io/org/app:v1.2.3
# Create PR previewgh pvdify preview
# Check deploy statusgh pvdify statusAPI
# Create releasecurl -X POST \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"image": "ghcr.io/org/app:v1.2.3"}' \ https://pvdify.win/api/v1/apps/myapp/releasesDeploy Hooks
Execute commands at deploy stages:
hooks: pre_deploy: - "npm run migrate" post_deploy: - "npm run cache:clear"Health Checks
Deploys wait for health check before switching:
healthcheck: path: /health interval: 5s timeout: 3s retries: 10Deploy fails if health check doesn’t pass within timeout.
Resource Limits
Each deploy respects resource constraints:
resources: memory: 512M cpu: 0.5Containers exceeding limits are killed by cgroups.
Logs
View deploy and runtime logs:
# Runtime logspvdify logs myapp -f
# Deploy logspvdify logs myapp --deploy
# Specific release logspvdify logs myapp --release 42Secrets Management
Secrets handled via SOPS encryption:
# Set secret (encrypted at rest)pvdify config:set --secret API_KEY=sk_live_...
# Secrets injected at container start# Never visible in logs or API responsesMonitoring
Integration with existing monitoring:
| Tool | Purpose | URL |
|---|---|---|
| PostHog | Product analytics | https://data.philoveracity.com |
| systemd journal | Log aggregation | journalctl -u pvdify-* |
| healthcheck | Liveness probes | /health endpoint |
PostHog Integration
Self-hosted PostHog at data.philoveracity.com provides shared analytics across all app brands:
# Set PostHog env vars for your apppvdify config:set myapp \ POSTHOG_HOST=https://data.philoveracity.com \ POSTHOG_KEY=phc_...Tracked across all apps:
- User events and funnels
- Feature flag evaluation
- Session recording (opt-in)
- Error tracking
Example: Full CI/CD Pipeline
name: CI/CD
on: push: branches: [main, develop] pull_request:
env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }}
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci - run: npm test
build: needs: test runs-on: ubuntu-latest outputs: image: ${{ steps.meta.outputs.tags }} steps: - uses: actions/checkout@v4
- name: Login to GHCR uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push uses: docker/build-push-action@v5 with: push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
deploy-staging: needs: build if: github.ref == 'refs/heads/develop' runs-on: ubuntu-latest steps: - run: | gh pvdify deploy myapp-staging \ --image ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} env: PVDIFY_TOKEN: ${{ secrets.PVDIFY_TOKEN }}
deploy-production: needs: build if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - run: | gh pvdify deploy myapp \ --image ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} env: PVDIFY_TOKEN: ${{ secrets.PVDIFY_TOKEN }}
preview: needs: build if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - run: | gh pvdify preview --pr ${{ github.event.number }} \ --image ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} env: PVDIFY_TOKEN: ${{ secrets.PVDIFY_TOKEN }}