deploy-cloud.yml 3.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. name: "Deploy: app.torqvoice.com"
  2. on:
  3. workflow_dispatch:
  4. inputs:
  5. tag:
  6. description: "Image tag to deploy (e.g. latest, v1.3.0, dev-abc1234)"
  7. required: false
  8. default: "latest"
  9. jobs:
  10. deploy:
  11. runs-on: [self-hosted, Linux, X64, hetzner]
  12. steps:
  13. - name: Log in to GitHub Container Registry
  14. run: echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
  15. - name: Create deployment files
  16. env:
  17. VIRTUAL_HOST: ${{ secrets.CLOUD_VIRTUAL_HOST }}
  18. DATABASE_URL: ${{ secrets.CLOUD_DATABASE_URL }}
  19. BETTER_AUTH_SECRET: ${{ secrets.CLOUD_BETTER_AUTH_SECRET }}
  20. APP_URL: ${{ secrets.CLOUD_APP_URL }}
  21. STRIPE_SECRET_KEY: ${{ secrets.CLOUD_STRIPE_SECRET_KEY }}
  22. STRIPE_WEBHOOK_SECRET: ${{ secrets.CLOUD_STRIPE_WEBHOOK_SECRET }}
  23. STRIPE_PRO_PRICE_ID: ${{ secrets.CLOUD_STRIPE_PRO_PRICE_ID }}
  24. STRIPE_ENTERPRISE_PRICE_ID: ${{ secrets.CLOUD_STRIPE_ENTERPRISE_PRICE_ID }}
  25. DATA_PATH: ${{ secrets.DATA_PATH }}
  26. POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }}
  27. POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}
  28. run: |
  29. mkdir -p $HOME/torqvoice-deploy/prod
  30. cd $HOME/torqvoice-deploy/prod
  31. env | grep -E '^(VIRTUAL_HOST|DATA_PATH|DATABASE_URL|BETTER_AUTH_SECRET|APP_URL|STRIPE_|POSTHOG_)' > .env
  32. cat > docker-compose.yml << 'COMPOSE'
  33. networks:
  34. proxy:
  35. external: true
  36. name: proxy
  37. services:
  38. torqvoice-app:
  39. container_name: torqvoice-app
  40. image: ghcr.io/torqvoice/torqvoice:${APP_TAG:-latest}
  41. restart: unless-stopped
  42. volumes:
  43. - ${DATA_PATH}/torqvoice_data:/app/data
  44. expose:
  45. - "3000"
  46. environment:
  47. VIRTUAL_HOST: ${VIRTUAL_HOST}
  48. VIRTUAL_PORT: "3000"
  49. LETSENCRYPT_HOST: ${VIRTUAL_HOST}
  50. DATABASE_URL: ${DATABASE_URL}
  51. BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
  52. NEXT_PUBLIC_APP_URL: ${APP_URL}
  53. TORQVOICE_MODE: cloud
  54. STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY}
  55. STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET}
  56. STRIPE_PRO_PRICE_ID: ${STRIPE_PRO_PRICE_ID}
  57. STRIPE_ENTERPRISE_PRICE_ID: ${STRIPE_ENTERPRISE_PRICE_ID}
  58. POSTHOG_HOST: ${POSTHOG_HOST}
  59. POSTHOG_KEY: ${POSTHOG_KEY}
  60. networks:
  61. - proxy
  62. COMPOSE
  63. - name: Ensure data directory permissions
  64. run: |
  65. DATA_DIR="${{ secrets.DATA_PATH }}/torqvoice_data"
  66. mkdir -p "$DATA_DIR"
  67. docker run --rm -v "$DATA_DIR":/data alpine chown -R 1001:1001 /data
  68. - name: Check free disk space
  69. run: |
  70. MIN_GB=5
  71. DOCKER_ROOT=$(docker info --format '{{.DockerRootDir}}' 2>/dev/null || echo /var/lib/docker)
  72. AVAIL_KB=$(df -Pk "$DOCKER_ROOT" | awk 'NR==2 {print $4}')
  73. AVAIL_GB=$(awk "BEGIN {printf \"%.1f\", $AVAIL_KB/1024/1024}")
  74. echo "Free space on $DOCKER_ROOT: ${AVAIL_GB} GB (minimum ${MIN_GB} GB)"
  75. if [ "$AVAIL_KB" -lt $((MIN_GB * 1024 * 1024)) ]; then
  76. echo "::error::Not enough disk space to pull image: ${AVAIL_GB} GB free, need ${MIN_GB} GB. Aborting deploy."
  77. exit 1
  78. fi
  79. - name: Deploy
  80. run: |
  81. cd $HOME/torqvoice-deploy/prod
  82. APP_TAG=${{ inputs.tag }} docker compose up -d --pull always
  83. - name: Prune old images and build cache
  84. run: docker image prune -f && docker builder prune -f