Skip to content

GitHub Actions Deployment Workflow

This template automates deployment to the EgyGeeks server using GitHub Actions with a self-hosted runner.

How It Works

  1. Push code to GitHub (main branch)
  2. GitHub Actions runner on server detects push
  3. Pulls latest code
  4. Stops old containers
  5. Builds fresh Docker image
  6. Starts new containers
  7. Shows deployment status

Template

.github/workflows/deploy.yml
name: Deploy to EgyGeeks Server

on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  deploy:
    runs-on: [self-hosted]

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

      - name: Set up environment
        run: |
          echo "Deploying MyApp"
          echo "Commit: ${{ github.sha }}"
          echo "Branch: ${{ github.ref_name }}"

      - name: Stop old containers
        run: |
          docker compose -f docker-compose.yml down || true

      - name: Build Docker image
        run: |
          docker compose -f docker-compose.yml build --no-cache

      - name: Start new containers
        run: |
          docker compose -f docker-compose.yml up -d

      - name: Wait for services to be healthy
        run: |
          echo "Waiting for services to start..."
          sleep 5

      - name: Cleanup old images
        run: |
          docker image prune -f

      - name: Deployment summary
        run: |
          echo "✅ Deployment completed successfully!"
          echo "📚 App URL: https://app.egygeeks.com"
          echo "🐳 Containers:"
          docker compose ps
          echo ""
          echo "Container status:"
          docker ps --filter "name=my-app" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"

Setup Steps

1. Create Workflow File

In your project repository:

mkdir -p .github/workflows
# Copy template above to .github/workflows/deploy.yml

2. Customize

Replace these values:

  • MyApp → Your application name
  • https://app.egygeeks.com → Your domain
  • my-app → Your container name (matches docker-compose.yml)

3. Commit and Push

git add .github/workflows/deploy.yml
git commit -m "Add deployment workflow"
git push origin main

4. Monitor Deployment

View workflow runs in GitHub:

Your Repo → Actions tab → Deploy to EgyGeeks Server

Configuration Options

Trigger on Specific Branches

Deploy from multiple branches:

on:
  push:
    branches:
      - main
      - production
      - staging

Manual Deployment Only

Remove automatic deployment on push:

on:
  workflow_dispatch:  # Manual trigger only

Deploy on Tag

Deploy when creating a release:

on:
  push:
    tags:
      - 'v*'

Add Environment Variables

Pass secrets to the build:

- name: Build Docker image
  env:
    API_KEY: ${{ secrets.API_KEY }}
  run: |
    docker compose build --no-cache

Add secrets in GitHub:

Repo → Settings → Secrets and variables → Actions → New repository secret

Understanding the Steps

Checkout Code

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

Pulls your latest code from GitHub to the server.

Stop Old Containers

- name: Stop old containers
  run: |
    docker compose -f docker-compose.yml down || true
  • || true prevents errors if containers don't exist
  • Gracefully stops and removes old containers

Build Docker Image

- name: Build Docker image
  run: |
    docker compose -f docker-compose.yml build --no-cache

Why --no-cache?

Without --no-cache, Docker may use cached layers and not pick up your latest code changes. Always use --no-cache in CI/CD.

Start New Containers

- name: Start new containers
  run: |
    docker compose -f docker-compose.yml up -d
  • -d runs in detached mode (background)
  • Starts containers defined in docker-compose.yml

Cleanup Old Images

- name: Cleanup old images
  run: |
    docker image prune -f

Removes dangling images to save disk space.

Advanced Examples

Multi-Environment Deployment

name: Deploy

on:
  push:
    branches: [main, staging]

jobs:
  deploy:
    runs-on: [self-hosted]
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set environment
        id: env
        run: |
          if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
            echo "environment=production" >> $GITHUB_OUTPUT
            echo "domain=app.egygeeks.com" >> $GITHUB_OUTPUT
          else
            echo "environment=staging" >> $GITHUB_OUTPUT
            echo "domain=staging.egygeeks.com" >> $GITHUB_OUTPUT
          fi

      - name: Deploy
        run: |
          docker compose -f docker-compose.${{ steps.env.outputs.environment }}.yml down || true
          docker compose -f docker-compose.${{ steps.env.outputs.environment }}.yml up -d --build

Run Tests Before Deploy

jobs:
  test:
    runs-on: [self-hosted]
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Run tests
        run: |
          docker compose -f docker-compose.test.yml up --abort-on-container-exit
          docker compose -f docker-compose.test.yml down

  deploy:
    needs: test  # Only deploy if tests pass
    runs-on: [self-hosted]
    steps:
      # ... deployment steps

Slack Notifications

- name: Notify Slack
  if: always()
  uses: 8398a7/action-slack@v3
  with:
    status: ${{ job.status }}
    text: 'Deployment to production'
    webhook_url: ${{ secrets.SLACK_WEBHOOK }}

Troubleshooting

Workflow not triggering

Check runner status:

# On server
cd /actions-runner
./run.sh

Verify runner is online:

Repo → Settings → Actions → Runners

Build fails with "permission denied"

Runner user needs Docker permissions:

sudo usermod -aG docker github-runner

Containers not starting

Check logs in GitHub Actions:

Actions tab → Failed workflow → View logs

Check container logs on server:

docker compose logs -f

"docker compose: command not found"

Use modern Docker Compose (v2):

# ✅ Correct
docker compose up -d

# ❌ Old (don't use)
docker-compose up -d

Best Practices

1. Always Use --no-cache

Prevents stale builds:

docker compose build --no-cache

2. Clean Up Resources

Prevent disk space issues:

- name: Cleanup
  run: |
    docker image prune -f
    docker volume prune -f  # If you're sure

3. Health Check Wait

Give containers time to become healthy:

- name: Wait for health check
  run: |
    sleep 10
    docker ps

4. Rollback on Failure

- name: Deploy
  id: deploy
  run: |
    docker compose up -d

- name: Rollback on failure
  if: failure()
  run: |
    docker compose down
    docker compose -f docker-compose.backup.yml up -d

Multi-Container Template Traefik Documentation