Skip to main content

CI/CD Setup Guide

Complete guide for setting up automatic deployments to production using GitHub Actions.

Overview

This setup provides:

  • Automatic deployments when pushing to main branch
  • CI tests on pull requests (RSpec, RuboCop, Brakeman)
  • Zero-downtime deploys with health checks
  • Automatic rollback if deployment fails
  • Manual deployment trigger option

Architecture

Push to main 

GitHub Actions

SSH to server

Pull code

Build image

Run migrations

Restart services

Health check

Success/Rollback

Step 1: Generate SSH Key for GitHub Actions

Generate a dedicated SSH key for GitHub Actions deployments:

# On your local machine
ssh-keygen -t ed25519 -C "github-actions@bizradar" -f ~/.ssh/github_actions_deploy

# This creates:
# - ~/.ssh/github_actions_deploy (private key - for GitHub)
# - ~/.ssh/github_actions_deploy.pub (public key - for server)

Step 2: Add Public Key to Production Server

# Copy public key to server
ssh deploy@91.98.204.128 "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys" < ~/.ssh/github_actions_deploy.pub

# Test the key works
ssh -i ~/.ssh/github_actions_deploy deploy@91.98.204.128 "echo 'Connection successful'"

Step 3: Configure GitHub Secrets

Go to your GitHub repository: Settings → Secrets and variables → Actions → New repository secret

Add these secrets:

Required Secrets:

  1. PRODUCTION_SSH_KEY

    # Get the private key content
    cat ~/.ssh/github_actions_deploy
    • Copy the ENTIRE output (including -----BEGIN OPENSSH PRIVATE KEY----- and -----END OPENSSH PRIVATE KEY-----)
    • Paste into GitHub secret
  2. PRODUCTION_HOST

    91.98.204.128
  3. PRODUCTION_USER

    deploy
  4. PRODUCTION_PATH

    /opt/odapi

Optional Secrets (for notifications):

  1. SLACK_WEBHOOK_URL (if using Slack notifications)
  2. DISCORD_WEBHOOK_URL (if using Discord notifications)

Step 4: Set Up GitHub Environment

Go to: Settings → Environments → New environment

Create environment: production

Protection rules (recommended):

  • ✅ Required reviewers: Add yourself
  • ✅ Wait timer: 0 minutes
  • ✅ Deployment branches: Only main

This adds an approval step before production deployments.

Step 5: Verify Deployment Scripts on Server

Scripts should already be in place at:

  • /opt/odapi/scripts/deploy-production.sh
  • /opt/odapi/scripts/rollback-production.sh

Verify they're executable:

ssh deploy@91.98.204.128 "ls -la /opt/odapi/scripts/*.sh"

Should show -rwxr-xr-x (executable).

How It Works

Automatic Deployment (Push to main)

# Local machine
git add .
git commit -m "feat: add new feature"
git push origin main

What happens:

  1. GitHub Actions detects push to main
  2. Runs deployment workflow
  3. SSHs to production server
  4. Executes deployment script:
    • Pulls latest code
    • Builds Docker image with commit hash
    • Runs database migrations
    • Restarts services
    • Health check (30 retries)
    • Auto-rollback if health check fails

Timeline: ~3-5 minutes

Manual Deployment (From Local Machine)

# Using rake task
bundle exec rake remote:deploy

# Prompts for confirmation, then deploys

Manual Deployment (GitHub UI)

  1. Go to Actions tab in GitHub
  2. Select Deploy to Production workflow
  3. Click Run workflow
  4. Select branch: main
  5. Click Run workflow

CI Pipeline (Pull Requests)

When you create a PR:

git checkout -b feature/new-feature
git push origin feature/new-feature
# Create PR on GitHub

Runs automatically:

  1. RSpec Tests - Full test suite with PostgreSQL & Redis
  2. RuboCop - Code style checks
  3. Brakeman - Security vulnerability scan

PR must pass all checks before merging to main.

Rollback Process

If Deployment Fails

Automatic rollback happens if:

  • Health check fails after 30 attempts (60 seconds)
  • Migration fails
  • Docker build fails

Manual Rollback

View recent commits:

bundle exec rake remote:commits

Rollback to specific commit:

bundle exec rake remote:rollback[abc123f]

# Or SSH to server
ssh deploy@91.98.204.128
cd /opt/odapi
./scripts/rollback-production.sh abc123f

Monitoring Deployments

View GitHub Actions Logs

  1. Go to Actions tab in GitHub
  2. Click on latest workflow run
  3. Expand deployment steps to see logs

Monitor from Local Machine

# Watch deployment logs
bundle exec rake remote:logs

# Check service status
bundle exec rake remote:status

# Health check
bundle exec rake remote:health

Deployment Workflow Details

What Gets Deployed

  • All code from main branch
  • Environment variables from .env (already on server)
  • Docker image rebuild with latest code
  • Database migrations (if any)

What Doesn't Change

  • .env file (persists between deployments)
  • Database data (migrations only, no data loss)
  • Redis data (persists)
  • Elasticsearch indices (persist)
  • SSL certificates (managed by Traefik)

Zero-Downtime Strategy

  1. New image built while old container runs
  2. Migrations run before restart
  3. Services restart with new image
  4. Health check verifies application
  5. Old images cleaned up after success

Troubleshooting

Deployment Fails at Health Check

# Check logs
bundle exec rake remote:logs

# SSH to server and investigate
ssh deploy@91.98.204.128
cd /opt/odapi
docker compose -f docker-compose.production.yml ps
docker compose -f docker-compose.production.yml logs rails

SSH Connection Issues

# Test GitHub Actions SSH key
ssh -i ~/.ssh/github_actions_deploy deploy@91.98.204.128

# Verify key is in authorized_keys
ssh deploy@91.98.204.128 "cat ~/.ssh/authorized_keys | grep github-actions"

Migration Fails

Migrations run automatically during deployment. If they fail:

# SSH to server and check migration status
ssh deploy@91.98.204.128
cd /opt/odapi
docker compose -f docker-compose.production.yml run --rm rails bundle exec rails db:migrate:status

# Fix and re-run manually
docker compose -f docker-compose.production.yml run --rm rails bundle exec rails db:migrate

GitHub Actions Workflow Not Triggering

Check:

  1. Workflow file in .github/workflows/
  2. Branch name matches trigger (main)
  3. GitHub Actions enabled in repo settings

Best Practices

Before Pushing to Main

  1. Test locally

    bundle exec rspec
    bundle exec rubocop
    brakeman
  2. Create PR first

    • Push to feature branch
    • Create pull request
    • Wait for CI checks
    • Get code review
    • Merge to main
  3. Small, incremental changes

    • Easier to rollback if needed
    • Faster deployment
    • Lower risk

Deployment Checklist

  • All tests passing locally
  • RuboCop clean
  • Brakeman clean
  • Database migrations tested
  • Feature tested in development
  • Breaking changes documented
  • Dependent services updated (if needed)

Post-Deployment

# Verify deployment
bundle exec rake remote:health
bundle exec rake remote:status

# Monitor logs for 5 minutes
bundle exec rake remote:logs

# Smoke test critical features
curl https://api.bizradar.fr/up
# Test other critical endpoints

Emergency Procedures

Critical Bug in Production

  1. Immediate rollback

    bundle exec rake remote:rollback[previous-commit-hash]
  2. Fix on hotfix branch

    git checkout main
    git pull
    git checkout -b hotfix/critical-bug
    # Fix the bug
    git commit -m "hotfix: fix critical bug"
    git push origin hotfix/critical-bug
  3. Emergency deploy (skip PR if critical)

    git checkout main
    git merge hotfix/critical-bug
    git push origin main
    # Auto-deploys via GitHub Actions

Server Issues

If server is unresponsive:

# Check server status
ssh deploy@91.98.204.128 "uptime && df -h && free -h"

# Restart all services
bundle exec rake remote:restart_all

# Last resort: restart server (via Hetzner console)

Customization

Add Slack Notifications

Edit .github/workflows/deploy-production.yml:

- name: Notify Slack
if: always()
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}

Change Deployment Branch

Edit workflow trigger:

on:
push:
branches: [ production ] # Change from 'main'

Skip CI on Specific Commits

git commit -m "docs: update README [skip ci]"

Useful Commands Reference

# Deployment
bundle exec rake remote:deploy # Manual deploy
bundle exec rake remote:rollback[hash] # Rollback
bundle exec rake remote:commits # View commits

# Monitoring
bundle exec rake remote:status # Service status
bundle exec rake remote:health # Health check
bundle exec rake remote:logs # Tail logs
bundle exec rake remote:stats # Resource usage

# Management
bundle exec rake remote:console # Rails console
bundle exec rake remote:run[task,args] # Run rake task
bundle exec rake remote:migrate # Run migrations

Architecture Diagram

┌─────────────────┐
│ Developer │
│ (Local Machine)│
└────────┬────────┘
│ git push origin main

┌────────▼────────┐
│ GitHub │
│ (Repository) │
└────────┬────────┘
│ Webhook trigger

┌────────▼────────┐
│ GitHub Actions │
│ (CI/CD Runner) │
└────────┬────────┘
│ SSH connection

┌────────▼────────────────────┐
│ Production Server │
│ (91.98.204.128) │
│ │
│ 1. Pull latest code │
│ 2. Build Docker image │
│ 3. Run migrations │
│ 4. Restart services │
│ 5. Health check │
│ 6. Success/Rollback │
└─────────────────────────────┘

Next Steps

  1. ✅ Complete GitHub secrets setup
  2. ✅ Test deployment with small change
  3. ✅ Set up monitoring (optional)
  4. ✅ Configure notifications (optional)
  5. ✅ Document team deployment process

Support

For issues:

  1. Check GitHub Actions logs
  2. Check server logs: bundle exec rake remote:logs
  3. Review this documentation
  4. Check deployment script: /opt/odapi/scripts/deploy-production.sh