My Blog

Automating Deployments with GitHub Actions: A CI/CD Journey

Description: How I learned to automate deployments and why I'll never manually deploy again

Author: Caden Eberle | Published: 2026-02-26


Automating Deployments with GitHub Actions: A CI/CD Journey

I used to manually SSH into my server, pull code, restart services, and pray nothing broke. Then I discovered GitHub Actions. Here's how it transformed my workflow.

The Manual Deployment Problem

My old deployment process looked like this:

# SSH into server
ssh ec2-user@my-server.com

# Navigate to project
cd /home/ec2-user/my-app

# Pull latest code
git pull origin main

# Restart services
docker compose down
docker compose up -d

# Cross fingers and hope it worked
```bash

**Problems with this approach:**
- Takes 5-10 minutes every time
- Easy to forget steps
- No rollback if something breaks
- Can't deploy from my phone
- Boring and repetitive

## Enter GitHub Actions

GitHub Actions lets you automate workflows. Every time I push code to GitHub, it automatically:
1. Tests the code
2. SSHs into my server
3. Pulls the latest changes
4. Restarts the application
5. Notifies me of success or failure

All in under 2 minutes, with zero manual effort.

## My First Workflow

Here's the workflow file that changed everything (`.github/workflows/deploy.yml`):
```bash
name: Deploy to EC2

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v3
    
    - name: Deploy to EC2
      env:
        PRIVATE_KEY: ${{ secrets.EC2_SSH_KEY }}
        HOST: ${{ secrets.EC2_HOST }}
        USER: ec2-user
      run: |
        echo "$PRIVATE_KEY" > private_key.pem
        chmod 600 private_key.pem
        ssh -o StrictHostKeyChecking=no -i private_key.pem ${USER}@${HOST} '
          cd /home/ec2-user/devops-final &&
          git pull origin main &&
          docker compose down &&
          docker compose up -d
        '
```bash

Let me break down what this does:

### The Trigger
```bash
on:
  push:
    branches: [ main ]
```bash
Runs automatically whenever I push to the main branch. No button to click, no command to run.

### The Runner
```bash
runs-on: ubuntu-latest
```bash
GitHub spins up a fresh Ubuntu server to run my workflow. It's free for public repositories!

### The Magic
The workflow:
1. **Checks out code**: Downloads my repository
2. **Sets up SSH**: Creates a private key from my secrets
3. **Connects to EC2**: SSHs into my production server
4. **Deploys**: Pulls code and restarts containers

## The Debugging Nightmare

Getting this working wasn't easy. I spent **hours** debugging:

### Problem 1: SSH Key Format
```bash
# This didn't work (base64 encoded)
echo "$PRIVATE_KEY" | base64 -d > private_key.pem

# This worked (raw key)
echo "$PRIVATE_KEY" > private_key.pem
```bash

**Lesson**: Store the raw PEM file in secrets, not base64 encoded.

### Problem 2: Merge Conflicts
The workflow would pull code, but then I'd have merge conflicts locally. Solution:
```bash
# Always pull before pushing
git pull --rebase origin main
git push origin main
```bash

### Problem 3: Wrong Repository Path
```bash
# Wrong (copied from template)
cd /path/to/your/repo

# Correct (my actual path)
cd /home/ec2-user/devops-final
```bash

**Lesson**: Always verify paths with `pwd` on your server!

## The Moment It Worked

After hours of failed deployments, I saw this:
```bash
✅ Deploy to EC2 completed successfully
```bash

I refreshed my website, and the changes were live. I literally stood up and cheered. This is the power of automation!

## Beyond Basic Deployment

Now that I have CI/CD working, I want to add:

### Automated Testing
```bash
- name: Run tests
  run: npm test
```bash

### Slack Notifications
```bash
- name: Notify team
  if: failure()
  run: |
    curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
      -d '{"text":"Deployment failed!"}'
```bash

### Staging Environment
Deploy to staging first, then production:
```bash
jobs:
  deploy-staging:
    # Deploy to staging server
  
  deploy-production:
    needs: deploy-staging
    # Only runs if staging succeeds
```bash

## CI/CD Best Practices I Learned

1. **Use secrets for sensitive data**: Never hardcode passwords or keys
2. **Test locally first**: Don't debug in production
3. **Start simple**: Get basic deployment working before adding complexity
4. **Read the logs**: GitHub Actions logs are incredibly detailed
5. **Version control everything**: Including workflow files

## The Real Benefits

After using GitHub Actions for a few weeks:

- **Time saved**: 10 minutes per deployment × 20 deployments = 200 minutes saved
- **Fewer errors**: No more forgotten steps
- **Confidence**: I deploy more often because it's easy
- **Documentation**: The workflow file IS the deployment process
- **Learning**: Understanding CI/CD is essential for modern development

## Resources

Getting started with GitHub Actions:
- [GitHub Actions Documentation](https://docs.github.com/en/actions)
- [Awesome Actions](https://github.com/sdras/awesome-actions) - Community workflows
- [GitHub Actions Marketplace](https://github.com/marketplace?type=actions) - Pre-built actions

## Final Thoughts

CI/CD isn't just for big companies with DevOps teams. With GitHub Actions, anyone can automate their deployments for free. The initial setup took me hours, but I'll save that time back in weeks.

If you're still manually deploying, stop. Set up GitHub Actions. Your future self will thank you.

**Never. Deploy. Manually. Again.**