TL;DR
CI runs your tests automatically on every push and pull request. CD deploys automatically once those tests pass. GitHub Actions does both from a YAML file in your repo, and it's free for public repositories. You push, the pipeline checks the code, and the server updates itself.
What is CI/CD?
Continuous Integration (CI) means every push is built and tested automatically, so a broken change is caught within minutes instead of at release time. Continuous Deployment (CD) goes one step further: when the tests pass on the main branch, the new version goes to production without anyone running commands by hand.
Companies like Google ship to production many times a day. That only works because every change goes through the same automated checks before it reaches users.
Why GitHub Actions?
If your code already lives on GitHub, Actions is the shortest path. It's free for public repositories and includes a monthly allowance of minutes for private ones. There's nothing extra to host: you add a YAML file under .github/workflows/ and GitHub runs it.
Your first workflow
name: Test & Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install -r requirements.txt
- name: Run tests
run: |
python manage.py test
- name: Check code style
run: |
pip install flake8
flake8 . --max-line-length=100
Deploying once the tests pass
deploy:
needs: test # Only runs if tests pass
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Deploy to VPS via SSH
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
cd /home/ubuntu/myapp
git pull origin main
docker compose build --no-cache
docker compose up -d
Keep SSH keys, passwords and API tokens in GitHub Secrets (Settings → Secrets and variables → Actions) and read them with ${{ secrets.NAME }}. Never hardcode them in a workflow file.
What to add next
- Coverage reports (coverage.py or pytest-cov)
- Push the Docker image to Docker Hub or GitHub Container Registry, and deploy that image instead of building on the server
- A Slack or Telegram message when a deployment succeeds or fails
- A staging environment that deploys automatically when a PR is merged
- An automatic rollback if the health check fails after a deploy
Why it's worth the setup
Once the pipeline is in place, nobody deploys by hand anymore. Every push is tested, every deployment runs the same steps, and most regressions are caught before users see them. The setup is a one-time cost that pays off on every release after it.