Skip to content

Deploy to DigitalOcean via Tailscale #34

Deploy to DigitalOcean via Tailscale

Deploy to DigitalOcean via Tailscale #34

Workflow file for this run

name: Deploy to DigitalOcean via Tailscale # Workflow name displayed in GitHub UI
# Deploys the latest stable tagged container images to production servers.
# Manual trigger with debug ssh option
on:
workflow_dispatch: # Manual trigger via GitHub UI
inputs:
enable_ssh_debugging: # Optional debugging parameter
description: 'Enable verbose SSH debugging' # Help text shown in UI
required: false # Not required to run the workflow
default: false # Disabled by default
type: boolean # Simple checkbox in the UI
permissions:
contents: read # Minimal permissions required for this workflow
jobs:
deploy:
name: Manual Deploy Over Tailscale # Display name for this job
runs-on: ubuntu-24.04 # Latest Ubuntu runner
steps:
# Step 1: Setup Tailscale on the GitHub Actions runner
- name: Setup Tailscale # Connect to the Tailscale network
uses: tailscale/github-action@v3 # Official Tailscale GitHub Action
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} # OAuth client ID for Tailscale
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }} # OAuth client secret for Tailscale
tags: tag:github-actions # Tag to identify this connection in Tailscale
version: latest # Use the latest version of Tailscale
use-cache: 'true' # Cache Tailscale binary for faster startup
# Step 2: (Debug only) Verifies SSH ED25519 SSH key
- name: Debug ED25519 Key # SSH key debugging step
if: ${{ inputs.enable_ssh_debugging == true }} # Only run when debugging is enabled
run: |
mkdir -p ~/.ssh
# Create SSH directory if it doesn't exist
echo "${{ secrets.DO_SSH_KEY }}" > ~/.ssh/id_ed25519
# Create private SSH key file
chmod 600 ~/.ssh/id_ed25519
# Set secure permissions on the key
echo "${{ secrets.DO_HOST_KEY }}" >> ~/.ssh/known_hosts
# Add host key to known hosts
ssh-keygen -l -f ~/.ssh/id_ed25519
# Show fingerprint of the key
ssh-keygen -y -f ~/.ssh/id_ed25519
# Show public key derived from private key
ssh -vvv -o StrictHostKeyChecking=accept-new -o BatchMode=yes -i ~/.ssh/id_ed25519 ${{ secrets.DO_USERNAME }}@${{ secrets.DO_TAILSCALE_NAME }} 'echo "Connection successful"'
# Test connection with verbose output
# Step 3: Create .env File Locally
- name: Create .env File Locally # Create environment file for deployment
run: |
cat > envfile <<EOF
# Start heredoc to create env file
# PostgreSQL
POSTGRES_USER=${{ secrets.POSTGRES_USER }}
# Database username
POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }}
# Database password
POSTGRES_DB=${{ secrets.POSTGRES_DB }}
# Database name
POSTGRES_HOST=postgres
# Container hostname for database
POSTGRES_PORT=${{ secrets.POSTGRES_PORT }}
# Database port
POSTGRES_SCHEMA=${{ secrets.POSTGRES_SCHEMA }}
# Database schema
DATABASE_URL=postgres://${{ secrets.POSTGRES_USER }}:${{ secrets.POSTGRES_PASSWORD }}@postgres:${{ secrets.POSTGRES_PORT }}/${{ secrets.POSTGRES_DB }}
# Connection string
# Backend
BACKEND_IMAGE_NAME=${{ secrets.BACKEND_IMAGE_NAME }}
# Docker image for backend
BACKEND_CONTAINER_NAME=${{ secrets.BACKEND_CONTAINER_NAME }}
# Container name for backend
BACKEND_PORT=${{ secrets.BACKEND_PORT }}
# Backend service port
BACKEND_INTERFACE=${{ secrets.BACKEND_INTERFACE }}
# Network interface for backend
BACKEND_ALLOWED_ORIGINS=${{ secrets.BACKEND_ALLOWED_ORIGINS }}
# CORS allowed origins
BACKEND_LOG_FILTER_LEVEL=${{ secrets.BACKEND_LOG_FILTER_LEVEL }}
# Logging level
BACKEND_SERVICE_PROTOCOL=${{ secrets.BACKEND_SERVICE_PROTOCOL }}
# Protocol for backend
BACKEND_SERVICE_HOST=${{ secrets.BACKEND_SERVICE_HOST }}
# Host for backend
BACKEND_API_VERSION=${{ secrets.BACKEND_API_VERSION }}
# API version
# TipTap
TIPTAP_URL=${{ secrets.TIPTAP_URL }}
# TipTap collaborative editor URL
TIPTAP_AUTH_KEY=${{ secrets.TIPTAP_AUTH_KEY }}
# TipTap authentication key
TIPTAP_JWT_SIGNING_KEY=${{ secrets.TIPTAP_JWT_SIGNING_KEY }}
# JWT signing key for TipTap
# Frontend
FRONTEND_IMAGE_NAME=${{ secrets.FRONTEND_IMAGE_NAME }}
# Docker image for frontend
FRONTEND_CONTAINER_NAME=${{ secrets.FRONTEND_CONTAINER_NAME }}
# Container name for frontend
FRONTEND_SERVICE_INTERFACE=${{ secrets.FRONTEND_SERVICE_INTERFACE }}
# Frontend service interface to listen for connections on
FRONTEND_SERVICE_PORT=${{ secrets.FRONTEND_SERVICE_PORT }}
# Frontend service port
# Platform
PLATFORM=${{ secrets.PLATFORM }}
# Target platform for Docker containers
EOF
# Step 4: SSH and deploy to the digitalocean droplet over private Tailscale tailnet
- name: SSH and Deploy with Docker Compose # Main deployment step
run: |
mkdir -p ~/.ssh
# Create SSH directory
echo "${{ secrets.DO_SSH_KEY }}" > ~/.ssh/id_ed25519
# Save SSH private key
chmod 600 ~/.ssh/id_ed25519
# Set secure permissions on key
echo "${{ secrets.DO_HOST_KEY }}" >> ~/.ssh/known_hosts
# Add static host key
ssh-keyscan -H ${{ secrets.DO_TAILSCALE_NAME }} >> ~/.ssh/known_hosts
# Add dynamic host key
# Copy .env to server
scp -o StrictHostKeyChecking=accept-new -i ~/.ssh/id_ed25519 envfile ${{ secrets.DO_USERNAME }}@${{ secrets.DO_TAILSCALE_NAME }}:/home/deploy/.env
# Upload env file
# SSH and deploy
ssh -o StrictHostKeyChecking=accept-new -o BatchMode=yes -i ~/.ssh/id_ed25519 ${{ secrets.DO_USERNAME }}@${{ secrets.DO_TAILSCALE_NAME }} '
set -e
# Exit immediately if any command fails
echo '📦 Starting deployment from branch: ${{ github.ref_name }}...'
cd /home/deploy
curl -O https://raw.githubusercontent.com/refactor-group/refactor-platform-rs/refs/heads/${{ github.ref_name }}/docker-compose.yaml
chmod 640 docker-compose.yaml
echo "🔧 Matching .env permissions to docker-compose.yaml..."
# Update env file permissions
chmod --reference=docker-compose.yaml .env
# Copy permissions from compose file
chown --reference=docker-compose.yaml .env
# Copy ownership from compose file
echo "📋 Showing masked .env:"
# Display environment with sensitive data masked
sed "s/POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=***/g; s/TIPTAP_AUTH_KEY=.*/TIPTAP_AUTH_KEY=***/g; s/TIPTAP_JWT_SIGNING_KEY=.*/TIPTAP_JWT_SIGNING_KEY=***/g" .env
# Print env with passwords hidden
echo "📥 Logging into GHCR..."
# Login to GitHub Container Registry
echo ${{ secrets.GHCR_PAT }} | docker login ghcr.io -u ${{ secrets.GHCR_USERNAME }} --password-stdin
# Docker login
echo "📥 Pulling images..."
# Pull Docker images
if [ -n "${{ secrets.BACKEND_IMAGE_NAME }}" ]; then
# Check if backend image is set
echo "Pulling backend image: ${{ secrets.BACKEND_IMAGE_NAME }}"
# Announce image pull
docker pull ${{ secrets.BACKEND_IMAGE_NAME }}
# Pull backend image
fi
if [ -n "${{ secrets.FRONTEND_IMAGE_NAME }}" ]; then
# Check if frontend image is set
echo "Pulling frontend image: ${{ secrets.FRONTEND_IMAGE_NAME }}"
# Announce image pull
docker pull ${{ secrets.FRONTEND_IMAGE_NAME }}
# Pull frontend image
fi
echo "🔍 Validating config..."
# Verify docker-compose configuration
docker compose config --quiet
# Check config without output unless error
echo "🛑 Stopping old containers..."
# Stop existing containers
docker compose down
# Stop and remove containers
echo "🚀 Starting new containers..."
# Start updated containers
docker compose up -d
# Start containers in detached mode
echo "⏳ Waiting for startup..."
# Wait for containers to start
sleep 15
# Pause for 15 seconds
echo "🩺 Checking status..."
# Check container status
docker ps -a
# List all containers
echo "🩺 Verifying app status..."
# Verify application health
echo "🩺 Checking rust-app status..."
if docker ps | grep -q rust-app; then
echo "✅ Deployment succeeded! ${{ secrets.BACKEND_CONTAINER_NAME }} is running."
# Success message
else
echo "⚠️ Missing container for rust-app. Logs follow:"
# Error message
docker logs ${{ secrets.BACKEND_CONTAINER_NAME }} --tail 30 2>/dev/null || echo "❌ Backend logs unavailable"
# Show backend logs
fi
echo "🎉 Deployment complete."
# Final deployment message
'