GitHub Actions Mastery: A Step-by-Step Guide to Fully Automated Deployments
Let’s be honest: manually deploying code is a waste of time and a recipe for human error. If you're still SSH-ing into servers or running git pull by hand, you're not just slowing yourself down — you're risking downtime, missed steps, and burnout. GitHub Actions gives you the power to automate the entire deployment pipeline, from test to production, right from your repo. Let’s walk through how to set up a reliable, repeatable, and fully automated deployment workflow.
1. Understand the Core: Workflows, Jobs, and Steps
GitHub Actions uses YAML files in .github/workflows to define workflows. Each workflow can have multiple jobs, and each job runs a series of steps — including running scripts, checking out code, or deploying.
Here’s a minimal example to get your head around the structure:
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.0.2
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_KEY }}
script: |
cd /var/www/myapp
git pull origin main
npm install
pm2 restart myapp
This runs on every push to main, checks out your code, and runs a deployment script over SSH. Simple, but effective.
2. Set Up Secrets — Never Hardcode Credentials
Never put passwords, API keys, or SSH keys in your YAML. Use GitHub Secrets instead.
Go to:
Repo Settings → Secrets and variables → Actions → New repository secret
Add these (example names):
-
HOST= your server IP or domain -
USERNAME= SSH user (e.g.,ubuntu) -
SSH_KEY= your private deploy key (use a key pair dedicated to CI)
You can generate a deploy key like this:
ssh-keygen -t rsa -b 4096 -C "github-actions@myapp.com" -f ./deploy-key -N ""
Add the public key (deploy-key.pub) to ~/.ssh/authorized_keys on your server.
3. Add Testing Before Deployment
Automated deployment is great — but only if your code works. Always run tests first.
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
# ... deployment steps
Now deploy only runs if test passes. This stops broken code from reaching production.
4. Use Environments for Safety
GitHub Environments add control and visibility. You can require approvals, set deployment branches, and store environment-specific secrets.
In GitHub:
Settings → Environments → New Environment → Name it production
Then update your workflow:
deploy:
environment: production
runs-on: ubuntu-latest
needs: test
steps:
# ... same as before
Now deployments to production show up in the Environments tab, and you can require manual approval:
environment:
name: production
url: https://myapp.com
Then in the environment settings, enable Required reviewers. Now every deploy needs a human thumbs-up.
5. Handle Rollbacks (Because Things Break)
Automated deployment should include a rollback plan. One way: tag your deploys and keep previous versions.
Example: use git tags to mark releases.
- name: Tag release
run: |
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git tag -a v${{ github.run_number }} -m "Release ${{ github.run_number }}"
git push origin v${{ github.run_number }}
Then, if something breaks, you can rollback with:
git checkout v123
git push origin main --force
Or better: build a rollback job in your workflow:
rollback:
runs-on: ubuntu-latest
needs: deploy
if: failure()
steps:
- name: Rollback to last tag
uses: appleboy/ssh-action@v1.0.2
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_KEY }}
script: |
cd /var/www/myapp
git checkout v$(git describe --tags --abbrev=0 --exclude='*rollback*')
pm2 restart myapp
This runs only if the deploy fails. Not perfect, but better than nothing.
6. Deploy to Different Stages: Staging First
Never deploy straight to production. Use staging.
yaml
on:
push:
branches: [ main, develop ]
jobs:
test:
# ... same as
---
☕ **Playful**
Top comments (0)