Deploy to DigitalOcean via Tailscale #34
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
' |