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:
- Backend Deployment (DataService)
- Frontend Deployment (Blazor WebAssembly) - only if backend succeeds
- 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 -rcreates timestamped backup - Permission fix:
sudo chownis CRITICAL - the backup operation changes ownership - Timestamp format:
YYYYMMDD_HHMMSSfor easy identification - Why permission fix?: The
sudo cpcommand makesrootthe owner, butServerUserneeds 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
ServerUsercan modify configuration files - Necessary because SCP uploaded as
ServerUserbut directory might be owned byBackendUser
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 -lshows full status without pagination - HTTP test:
curl -ffails on HTTP errors,-ssuppresses progress,-Isends 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
wwwrootcontent (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
- Backup Creation: Files become owned by
root(sudo cp) - Permission Reset: Change to
ServerUserfor deployment - File Upload: SCP uploads as
ServerUser - Configuration:
ServerUsercreates config files - Service Handoff: Change ownership to service ServerUser
- 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.jsonFrontend.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
📚 Related Documentation
- GitHub Actions Documentation
- SSH Action Documentation
- SCP Action Documentation
- .NET Deployment Guide
- Blazor WebAssembly Hosting
Last Updated: August 2025
Version: 1.0
Maintainer: EnergyTracker Development Team