Skip to content

Deploy to Droplet

Deploy to Droplet #564

# Deploy to DigitalOcean Droplet
# Deploys the application to a DigitalOcean Droplet via rsync + SSH
# Requires UDP ports open for BitTorrent DHT to work
#
# This workflow runs AFTER the CI workflow completes successfully
# to avoid duplicate test runs
name: Deploy to Droplet
on:
# Run after CI workflow completes successfully
workflow_run:
workflows: ["CI Pipeline"]
types: [completed]
branches: [main, master]
workflow_dispatch: # Allow manual trigger
concurrency:
group: deploy-droplet
cancel-in-progress: true
env:
# Deploy path: /home/ubuntu/www/:domain/:repo
DEPLOY_PATH: /home/ubuntu/www/bittorrented.com/media-streamer
SERVICE_NAME: bittorrented
jobs:
# Deploy to Droplet (only if CI passed)
deploy:
name: Deploy to Droplet
runs-on: ubuntu-latest
# Only deploy if CI workflow succeeded (or manual trigger)
if: |
github.event_name == 'workflow_dispatch' ||
(github.event.workflow_run.conclusion == 'success' &&
(github.event.workflow_run.head_branch == 'main' || github.event.workflow_run.head_branch == 'master'))
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Write .env file
env:
ENV_FILE_CONTENT: ${{ secrets.ENV_FILE }}
run: |
printf '%s\n' "$ENV_FILE_CONTENT" > .env
- name: Setup SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.DROPLET_SSH_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
# Add host to known_hosts (with retry for transient failures)
for i in 1 2 3; do
ssh-keyscan -p ${{ secrets.DROPLET_PORT || 22 }} -H ${{ secrets.DROPLET_HOST }} >> ~/.ssh/known_hosts 2>/dev/null && break
sleep 2
done
- name: Sync files to Droplet (with retry)
uses: nick-fields/retry@v3
with:
timeout_minutes: 15
max_attempts: 5
retry_wait_seconds: 5
command: |
SSH_OPTS="-i ~/.ssh/deploy_key -p ${{ secrets.DROPLET_PORT || 22 }} -o StrictHostKeyChecking=no -o ConnectTimeout=60 -o ServerAliveInterval=30 -o ServerAliveCountMax=5"
# Create deploy directory if it doesn't exist
ssh $SSH_OPTS ${{ secrets.DROPLET_USER }}@${{ secrets.DROPLET_HOST }} "mkdir -p ${{ env.DEPLOY_PATH }}"
# Sync files
rsync -azv --partial --progress --delete \
--exclude='.git' \
--exclude='node_modules' \
--exclude='.next' \
--exclude='.env.local' \
--exclude='*.log' \
-e "ssh $SSH_OPTS" \
./ ${{ secrets.DROPLET_USER }}@${{ secrets.DROPLET_HOST }}:${{ env.DEPLOY_PATH }}/
- name: Start build on Droplet (background)
run: |
SSH_OPTS="-i ~/.ssh/deploy_key -p ${{ secrets.DROPLET_PORT || 22 }} -o StrictHostKeyChecking=no -o ConnectTimeout=60"
# Create the build script on the server
ssh $SSH_OPTS ${{ secrets.DROPLET_USER }}@${{ secrets.DROPLET_HOST }} << 'SETUP_SCRIPT'
cat > /tmp/deploy-build.sh << 'BUILD_SCRIPT'
#!/bin/bash
set -e
LOG_FILE="/tmp/deploy-build.log"
STATUS_FILE="/tmp/deploy-build.status"
# Clear previous status
echo "running" > "$STATUS_FILE"
{
cd /home/ubuntu/www/bittorrented.com/media-streamer
# Run idempotent setup script
echo "=== Running setup script ==="
bash scripts/setup-server.sh
# Source profile to get pnpm in PATH
export PNPM_HOME="$HOME/.local/share/pnpm"
export PATH="$PNPM_HOME:$HOME/.pnpm:$HOME/.npm-global/bin:/usr/local/bin:$PATH"
[ -f ~/.bashrc ] && source ~/.bashrc 2>/dev/null || true
[ -f ~/.profile ] && source ~/.profile 2>/dev/null || true
echo ""
echo "=== Environment ==="
echo "PATH: $PATH"
echo "Node: $(node --version 2>/dev/null || echo 'not found')"
echo "pnpm: $(which pnpm 2>/dev/null || echo 'not found')"
echo ""
echo "=== System resources ==="
free -h
df -h /
echo ""
echo "=== Cleaning build cache ==="
# Acquire exclusive lock — wait for any previous deploy to finish
exec 200>/tmp/deploy-build.lock
flock -w 300 200 || { echo "✗ Lock timeout after 300s"; echo "failed" > "$STATUS_FILE"; exit 1; }
# Kill any lingering next build processes
pkill -f "next build" 2>/dev/null || true
sleep 1
rm -rf .next 2>/dev/null || true
echo "✓ Build cache cleaned"
echo ""
echo "=== Installing dependencies ==="
pnpm install --frozen-lockfile
echo "✓ Dependencies installed"
echo ""
echo "=== Building application ==="
nice -n 10 pnpm build || { echo "✗ Build FAILED"; echo "failed" > "$STATUS_FILE"; exit 1; }
echo "✓ Build complete"
echo ""
# Verify build output exists before restarting
if [ ! -f .next/standalone/server.js ]; then
echo "✗ Build output missing (.next/standalone/server.js not found)"
echo "failed" > "$STATUS_FILE"
exit 1
fi
echo "=== Restarting services ==="
sudo systemctl restart bittorrented || echo "Warning: Could not restart main service"
echo "✓ Main service restart attempted"
sudo systemctl restart bittorrented-iptv-worker || echo "Warning: Could not restart IPTV worker"
echo "✓ IPTV worker restart attempted"
sleep 3
echo ""
echo "=== Verifying deployment ==="
systemctl is-active bittorrented || echo "Main service status unknown"
systemctl is-active bittorrented-iptv-worker || echo "IPTV worker status unknown"
echo ""
echo "=== Deployment complete! ==="
echo "success" > "$STATUS_FILE"
} >> "$LOG_FILE" 2>&1 || {
echo "failed" > "$STATUS_FILE"
exit 1
}
BUILD_SCRIPT
chmod +x /tmp/deploy-build.sh
SETUP_SCRIPT
# Start the build in background using nohup
ssh $SSH_OPTS ${{ secrets.DROPLET_USER }}@${{ secrets.DROPLET_HOST }} \
"nohup /tmp/deploy-build.sh > /dev/null 2>&1 &"
echo "Build started in background on droplet"
- name: Wait for build to complete
run: |
SSH_OPTS="-i ~/.ssh/deploy_key -p ${{ secrets.DROPLET_PORT || 22 }} -o StrictHostKeyChecking=no -o ConnectTimeout=30"
echo "Waiting for build to complete..."
MAX_WAIT=600 # 10 minutes
ELAPSED=0
POLL_INTERVAL=10
while [ $ELAPSED -lt $MAX_WAIT ]; do
# Check build status
STATUS=$(ssh $SSH_OPTS ${{ secrets.DROPLET_USER }}@${{ secrets.DROPLET_HOST }} \
"cat /tmp/deploy-build.status 2>/dev/null || echo 'unknown'" || echo "ssh_error")
if [ "$STATUS" = "success" ]; then
echo "✓ Build completed successfully!"
# Show the log
ssh $SSH_OPTS ${{ secrets.DROPLET_USER }}@${{ secrets.DROPLET_HOST }} \
"tail -50 /tmp/deploy-build.log" || true
exit 0
elif [ "$STATUS" = "failed" ]; then
echo "✗ Build failed!"
ssh $SSH_OPTS ${{ secrets.DROPLET_USER }}@${{ secrets.DROPLET_HOST }} \
"cat /tmp/deploy-build.log" || true
exit 1
elif [ "$STATUS" = "ssh_error" ]; then
echo "SSH connection failed, retrying in ${POLL_INTERVAL}s..."
else
echo "Build still running... (${ELAPSED}s elapsed)"
fi
sleep $POLL_INTERVAL
ELAPSED=$((ELAPSED + POLL_INTERVAL))
done
echo "✗ Build timed out after ${MAX_WAIT}s"
ssh $SSH_OPTS ${{ secrets.DROPLET_USER }}@${{ secrets.DROPLET_HOST }} \
"cat /tmp/deploy-build.log" || true
exit 1
- name: Cleanup SSH key
if: always()
run: rm -f ~/.ssh/deploy_key