Skip to content

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 deploy

Why External CI?

ConstraintReason
No builds on VPSCPU/memory reserved for running apps
Kernel 3.10Limited build tooling compatibility
SecurityBuild-time secrets stay in CI
ConsistencySame image runs everywhere

Workflow: Standard Deploy

1. Push Code

Terminal window
git push origin main

2. CI Builds Image (GitHub Actions)

.github/workflows/deploy.yml
name: Deploy
on:
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

Terminal window
# Triggered by CI or manually
pvdify deploy myapp --image ghcr.io/org/app:abc123

4. Blue-Green Swap

┌─────────────────────────────────────────────────────┐
│ Cloudflare Tunnel │
└───────────────────────────┬─────────────────────────┘
┌─────────────┴─────────────┐
▼ ▼
┌────────────┐ ┌────────────┐
│ Blue (v1) │ │ Green (v2) │
│ :3001 │ │ :3002 │
│ (active) │ ──swap──▶ │ (active) │
└────────────┘ └────────────┘
  1. New container starts on alternate port
  2. Health check passes
  3. Tunnel route switches to new port
  4. Old container stops (after drain period)

Workflow: PR Previews

Automatic preview environments for pull requests:

GitHub Action

.github/workflows/preview.yml
name: Preview
on:
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

EventAction
PR openedCreate preview slot
PR updatedUpdate preview
PR merged/closedDelete preview (24h delay)

Workflow: Rollback

Instant rollback to previous release:

Terminal window
# Rollback to previous
pvdify rollback myapp
# Rollback to specific version
pvdify rollback myapp --version 41
# View release history
pvdify releases myapp

Rollback is instant because:

  • Previous image is cached
  • Previous config version is stored
  • Just a port swap operation

Deploy Commands

CLI

Terminal window
# Deploy specific image
pvdify deploy myapp --image ghcr.io/org/app:v1.2.3
# Deploy with config change
pvdify config:set DATABASE_URL=... && pvdify deploy myapp
# Deploy to staging
pvdify deploy myapp-staging --image ghcr.io/org/app:v1.2.3

GitHub Extension

Terminal window
# Deploy current branch
gh pvdify deploy
# Deploy specific image
gh pvdify deploy --image ghcr.io/org/app:v1.2.3
# Create PR preview
gh pvdify preview
# Check deploy status
gh pvdify status

API

Terminal window
# Create release
curl -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/releases

Deploy Hooks

Execute commands at deploy stages:

pvdify.yaml
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: 10

Deploy fails if health check doesn’t pass within timeout.

Resource Limits

Each deploy respects resource constraints:

resources:
memory: 512M
cpu: 0.5

Containers exceeding limits are killed by cgroups.

Logs

View deploy and runtime logs:

Terminal window
# Runtime logs
pvdify logs myapp -f
# Deploy logs
pvdify logs myapp --deploy
# Specific release logs
pvdify logs myapp --release 42

Secrets Management

Secrets handled via SOPS encryption:

Terminal window
# Set secret (encrypted at rest)
pvdify config:set --secret API_KEY=sk_live_...
# Secrets injected at container start
# Never visible in logs or API responses

Monitoring

Integration with existing monitoring:

ToolPurposeURL
PostHogProduct analyticshttps://data.philoveracity.com
systemd journalLog aggregationjournalctl -u pvdify-*
healthcheckLiveness probes/health endpoint

PostHog Integration

Self-hosted PostHog at data.philoveracity.com provides shared analytics across all app brands:

Terminal window
# Set PostHog env vars for your app
pvdify 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

.github/workflows/ci-cd.yml
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 }}