Table of Contents

Deployment Workflow Documentation

📋 Overview

This document provides a comprehensive, step-by-step explanation of the automated CI/CD deployment pipeline for the Soft-Creation system. The pipeline is implemented using GitHub Actions and deploys both backend and frontend components to a Ubuntu server.

🎯 Workflow Triggers

Tag-Based Deployment

on:
  push:
    tags:
      - 'v*'  # Triggers on tags like v1.0.0, v1.1.0, v2.0.0, etc.

Why tag-based?

  • Intentional deployments: Prevents accidental deployments on every commit
  • Version control: Clear versioning strategy (semantic versioning)
  • Rollback capability: Easy to identify and revert to specific versions
  • Production safety: Deployment only happens when explicitly tagged

How to trigger a deployment:

git tag v1.0.5
git push origin v1.0.5

🏗️ Architecture Overview

The deployment pipeline follows a sequential deployment strategy:

  1. Backend Deployment (DataService)
  2. Frontend Deployment (Blazor WebAssembly) - only if backend succeeds
  3. Success/Failure Notifications

This ensures that the frontend always has a working backend to connect to.

🔐 Secret Management

GitHub Secrets Required

Secret Name Purpose Example Value
SERVER_HOST Target server IP 0.0.0.0
SERVER_ServerUser SSH ServerUsername ServerUsername
SSH_PRIVATE_KEY SSH authentication -----BEGIN OPENSSH PRIVATE KEY-----
MONGODB_CONNECTION_STRING Database connection mongodb://localhost:27017
MONGODB_DATABASE_NAME Database name DbName
JWT_SECRET_KEY JWT signing key [32+ character secure key]
FRONTEND_URL CORS origin https://front-end.yourdomain.com
API_BASE_URL Frontend API endpoint https://back-end.yourdomain.com

Template Replacement System

Templates in Git (safe to commit):

// appsettings.template.json
{
  "ConnectionStrings": {
    "MongoDb": "REPLACE_WITH_ACTUAL_CONNECTION_STRING"
  },
  "Jwt": {
    "SecretKey": "REPLACE_WITH_GENERATED_SECRET_KEY"
  }
}

Actual config on server (generated during deployment):

// appsettings.json (never in Git)
{
  "ConnectionStrings": {
    "MongoDb": "mongodb://localhost:27017"
  },
  "Jwt": {
    "SecretKey": "actualSecretKeyFromGitHubSecrets"
  }
}

🎯 Backend Deployment

Job: deploy-backend

1. Environment Setup

- name: Checkout code
  uses: actions/checkout@v4

Purpose: Downloads the Git repository content to the GitHub Actions runner.

- name: Setup .NET
  uses: actions/setup-dotnet@v4
  with:
    dotnet-version: ${{ env.DOTNET_VERSION }}

Purpose: Installs .NET 8 SDK on the runner. Uses a global environment variable for consistency.

2. Build Process

- name: Restore dependencies
  run: |
    cd DataService.Server
    dotnet restore

Purpose: Downloads all NuGet packages (equivalent to npm install for Node.js).

- name: Build and publish backend
  run: |
    cd Backend.Server
    dotnet publish -c Release -o ./publish --no-restore

Purpose:

  • Creates a production-ready application bundle
  • -c Release: Optimized build without debug symbols
  • -o ./publish: Output directory
  • --no-restore: Skips dependency restoration (already done)

3. Build Verification

- name: Verify publish output
  run: |
    echo "Backend publish contents:"
    ls -la Backend.Server/publish/

Purpose: Debugging step that shows what files were created. Essential for troubleshooting build issues.

4. Service Management - Stop

- name: Stop Backend on server
  uses: appleboy/ssh-action@v1.0.3
  with:
    host: ${{ secrets.SERVER_HOST }}
    ServerUsername: ${{ secrets.SERVER_ServerUser }}
    key: ${{ secrets.SSH_PRIVATE_KEY }}
    script: |
      echo "Stopping Backend..."
      sudo systemctl stop backend
      echo "Service stopped successfully"

Purpose:

  • Stops the running service to prevent file locking issues
  • Uses SSH key authentication (no passwords)
  • Essential for zero-downtime deployment preparation

5. Backup Strategy

- name: Create backup of current backend
  uses: appleboy/ssh-action@v1.0.3
  with:
    host: ${{ secrets.SERVER_HOST }}
    ServerUsername: ${{ secrets.SERVER_ServerUser }}
    key: ${{ secrets.SSH_PRIVATE_KEY }}
    script: |
      echo "Creating backup..."
      sudo cp -r /var/www/apps/Backend /var/www/apps/Backend.backup.$(date +%Y%m%d_%H%M%S)
      echo "Ensuring ServerUser can write to Backend directory..."
      sudo chown -R ServerUser:ServerUser /var/www/apps/Backend/
      echo "Backup created successfully"

Purpose & Critical Details:

  • Backup creation: sudo cp -r creates timestamped backup
  • Permission fix: sudo chown is CRITICAL - the backup operation changes ownership
  • Timestamp format: YYYYMMDD_HHMMSS for easy identification
  • Why permission fix?: The sudo cp command makes root the owner, but ServerUser needs write access for SCP upload

6. File Upload

- name: Copy backend files to server
  uses: appleboy/scp-action@v0.1.7
  with:
    host: ${{ secrets.SERVER_HOST }}
    ServerUsername: ${{ secrets.SERVER_ServerUser }}
    key: ${{ secrets.SSH_PRIVATE_KEY }}
    source: "Backend.Server/publish/*"
    target: "/var/www/apps/Backend"
    strip_components: 2
    overwrite: true

SCP Details:

  • Protocol: Secure Copy Protocol over SSH
  • Source: All files in the publish directory
  • strip_components: 2: Removes "Backend.Server/publish/" from paths
  • Example: Backend.Server/publish/app.dll/var/www/apps/Backend/app.dll
  • overwrite: true: Replaces existing files

7. Configuration & Permission Management

- name: Set permissions and start DataService
  uses: appleboy/ssh-action@v1.0.3
  with:
    host: ${{ secrets.SERVER_HOST }}
    ServerUsername: ${{ secrets.SERVER_ServerUser }}
    key: ${{ secrets.SSH_PRIVATE_KEY }}
    script: |
      echo "Setting permissions for deployment..."
      sudo chown -R ServerUser:ServerUser /var/www/apps/Backend/
      
      echo "Copy and configure backend appsettings..."
      cd /var/www/apps/Backend
      if [ -f appsettings.template.json ]; then
        cp appsettings.template.json appsettings.json
        
        # Replace template placeholders with actual values
        sed -i 's|REPLACE_WITH_ACTUAL_CONNECTION_STRING|${{ secrets.MONGODB_CONNECTION_STRING }}|g' appsettings.json
        sed -i 's|REPLACE_WITH_DATABASE_NAME|${{ secrets.MONGODB_DATABASE_NAME }}|g' appsettings.json
        sed -i 's|REPLACE_WITH_FRONTEND_URL|${{ secrets.FRONTEND_URL }}|g' appsettings.json
        sed -i 's|REPLACE_WITH_GENERATED_SECRET_KEY|${{ secrets.JWT_SECRET_KEY }}|g' appsettings.json
        
        echo "Backend configuration placeholders replaced with actual values"
        echo "Configuration preview (masked):"
        sed 's/SecretKey.*/SecretKey": "[MASKED]"/g' appsettings.json
      else
        echo "Error: appsettings.template.json not found!"
        exit 1
      fi
      
      echo "Setting service permissions..."
      sudo chown -R ServerUser:ServerUser /var/www/apps/Backend/
      sudo chmod +x /var/www/apps/DataService/Backend.Server

This is the most complex part. Step by step:

7a. Deployment Permissions

sudo chown -R ServerUser:ServerUser /var/www/apps/Backend/
  • Ensures ServerUser can modify configuration files
  • Necessary because SCP uploaded as ServerUser but directory might be owned by BackendUser

7b. Configuration Generation

cp appsettings.template.json appsettings.json
sed -i 's|PLACEHOLDER|ACTUAL_VALUE|g' appsettings.json
  • Template system: Secrets are never stored in Git
  • sed replacement: Uses | delimiter (safer for URLs than /)
  • GitHub Actions substitution: ${{ secrets.XYZ }} is replaced with actual secret values
  • Security: Logs show masked values to prevent secret exposure

7c. Service Permissions

sudo chown -R BackendUser:BackendUser /var/www/apps/Backend/
sudo chmod +x /var/www/apps/Backend/Backend.Server
  • Security best practice: Service runs with dedicated ServerUser (not admin)
  • Executable permission: Ensures the .NET application can be executed
  • Principle of least privilege: Service has only necessary permissions

8. Service Startup & Health Checks

echo "Starting Backend..."
sudo systemctl start backend

echo "Waiting for service to start..."
sleep 5

echo "Checking service status..."
sudo systemctl status backend --no-pager -l

echo "Testing API endpoint..."
curl -f -s -I http://localhost:7252/ || echo "Warning: API endpoint test failed"

Health Check Details:

  • systemctl start: Starts the systemd service
  • sleep 5: Gives the .NET application time to initialize
  • status check: --no-pager -l shows full status without pagination
  • HTTP test: curl -f fails on HTTP errors, -s suppresses progress, -I sends HEAD request
  • Graceful failure: || echo "Warning..." continues deployment even if test fails

🎨 Frontend Deployment (Blazor WebAssembly)

Job: deploy-frontend

Dependency: Only runs if deploy-backend succeeds (needs: deploy-backend)

1. Build Process (Similar to Backend)

- name: Build and publish frontend
  run: |
    cd Frontend.Client
    dotnet publish -c Release -o ./publish --no-restore

Blazor WebAssembly Specifics:

  • Produces static files (HTML, CSS, JS, WASM)
  • No server process (unlike backend)
  • Output goes to publish/wwwroot/ directory

2. Frontend File Upload

- name: Copy frontend files to server
  uses: appleboy/scp-action@v0.1.7
  with:
    source: "Frontend.Client/publish/wwwroot/*"
    target: "/var/www/Frontend"
    strip_components: 3

Frontend-specific details:

  • Source: Only the wwwroot content (web-ready files)
  • strip_components: 3: Removes "Frontend.Client/publish/wwwroot/"
  • Target: Direct to Apache document root

3. Frontend Configuration & Permissions

echo "Copy and configure frontend appsettings..."
cd /var/www/Frontend
if [ -f appsettings.template.json ]; then
  cp appsettings.template.json appsettings.json
  
  # Replace template placeholders with actual values
  sed -i 's|REPLACE_WITH_API_BASE_URL|${{ secrets.API_BASE_URL }}|g' appsettings.json
  
  echo "Frontend configuration placeholders replaced with actual values"
  echo "Frontend configuration:"
  cat appsettings.json
else
  echo "Warning: Frontend appsettings.template.json not found!"
fi

echo "Setting web server permissions..."
sudo chown -R www-data:www-data /var/www/Frontend/

Frontend Configuration:

  • Simpler than backend: Only needs API base URL
  • www-data ownership: Apache ServerUser needs read access
  • No executable permissions needed: Static files only

4. Apache Management

echo "Starting Apache..."
sudo systemctl start apache2

echo "Testing frontend endpoint..."
curl -f -s -I https://frontend.yourdomain.com/ || echo "Warning: Frontend test failed"

Key differences from backend:

  • Apache2 instead of custom service
  • External URL test: Tests complete chain (nginx → Apache)
  • HTTPS: Full production URL with SSL

🔄 Permission Management Strategy

The Two-ServerUser System

Administrative ServerUser: ServerUser

  • Used for deployments
  • Has sudo privileges
  • Uploads files via SCP

Service ServerUsers:

  • Backend: BackendUser (application execution)
  • Frontend: www-data (web server)

Permission Flow

  1. Backup Creation: Files become owned by root (sudo cp)
  2. Permission Reset: Change to ServerUser for deployment
  3. File Upload: SCP uploads as ServerUser
  4. Configuration: ServerUser creates config files
  5. Service Handoff: Change ownership to service ServerUser
  6. Service Start: Service runs with minimal privileges

Why This Approach?

  • Security: Services don't run with admin privileges
  • Deployment: Admin ServerUser can deploy without service interruption
  • Isolation: Compromised service can't affect system
  • Auditability: Clear separation of concerns

🚨 Troubleshooting Guide

Common Issues

Permission Denied During Upload

tar: file.dll: Cannot open: Permission denied

Cause: Upload directory not owned by ServerUser Solution:

sudo chown -R ServerUser:ServerUser /var/www/apps/Backend/
sudo chown -R ServerUser:ServerUser /var/www/Frontend/

Service Fails to Start

systemctl status shows failed state

Check logs:

sudo journalctl -u backend.service -f

Common causes:

  • Configuration file errors
  • Missing executable permissions
  • Database connection issues

CORS Errors in Browser

CORS policy: No 'Access-Control-Allow-Origin' header

Check backend config:

cat /var/www/apps/Backend/appsettings.json

Verify CORS AllowedOrigins contains correct frontend URL

Template Replacement Fails

sed: can't read appsettings.template.json: No such file

Ensure template files exist in project:

  • Backend.Server/appsettings.template.json
  • Frontend.Client/wwwroot/appsettings.template.json

Manual Recovery

Rollback to Previous Version

# Stop services
sudo systemctl stop dataservice
sudo systemctl stop apache2

# Find backup
ls -la /var/www/apps/Backend.backup.*
ls -la /var/www/Frontend.backup.*

# Restore from backup
sudo rm -rf /var/www/apps/Backend
sudo mv /var/www/apps/Backend.backup.YYYYMMDD_HHMMSS /var/www/apps/Backend

# Fix permissions and restart
sudo chown -R BackendUser:BackendUser /var/www/apps/Backend/
sudo systemctl start backend

Force Configuration Regeneration

cd /var/www/apps/Backend
sudo cp appsettings.template.json appsettings.json
# Manually edit appsettings.json with correct values
sudo systemctl restart backend

📊 Monitoring & Logging

Deployment Monitoring

  • GitHub Actions: Repository → Actions → Workflow runs
  • Real-time logs: Watch live deployment progress
  • Artifacts: Download build outputs if needed

Service Monitoring

# Backend service
sudo systemctl status backend
sudo journalctl -u backend.service -f

# Web server
sudo systemctl status apache2
sudo tail -f /var/log/apache2/frontend.log

# System resources
htop
df -h

Health Check Endpoints

# Backend API
curl -I https://backend.yourdomain.com/

# Frontend
curl -I https://frontend.yourdomain.com/

# Internal backend
curl -I http://localhost:7252/

🔐 Security Considerations

Secrets Management

  • Never commit secrets to Git
  • Use GitHub Secrets for all sensitive data
  • Rotate secrets regularly
  • Monitor secret access logs

SSH Security

  • Use SSH keys, never passwords
  • Restrict SSH key access to specific hosts
  • Consider using dedicated deployment keys

Service Security

  • Run services with minimal privileges
  • Regular security updates
  • Monitor service logs for anomalies
  • Use HTTPS for all external communication

🚀 Performance Optimization

Build Optimization

  • --no-restore: Skip unnecessary dependency downloads
  • Release configuration: Optimized builds
  • Parallel execution: Backend and frontend jobs can run independently

Deployment Speed

  • Incremental uploads: Only changed files
  • Backup strategy: Fast local copies
  • Health checks: Quick verification

Resource Usage

  • Minimal downtime: Services restart quickly
  • Efficient file operations: Use of strip_components
  • Logging optimization: Targeted log output

📈 Future Enhancements

Potential Improvements

  • Blue-green deployments: Zero-downtime deployments
  • Database migrations: Automated schema updates
  • Load balancing: Multiple application instances
  • Environment-specific configs: Development, staging, production
  • Automated testing: Integration tests before deployment
  • Slack/Teams notifications: Real-time deployment status

Scalability Considerations

  • Multiple server support: Deploy to server clusters
  • Container deployment: Docker/Kubernetes integration
  • CDN integration: Static asset optimization
  • Database clustering: MongoDB replica sets


Last Updated: August 2025
Version: 1.0
Maintainer: EnergyTracker Development Team