24.6. GitHub Actions
Table of Contents
Why GitHub Actions Matters for Backend Developers
GitHub Actions is GitHub’s built‑in CI/CD system. It lets you automatically run tests, build Docker images, lint code, or deploy your backend every time you push code or open a pull request.
For a backend developer, GitHub Actions is often the quickest way to:
- Run your test suite on every push.
- Build and publish Docker images.
- Automatically deploy to a server or platform.
You define all of this as code in YAML files stored in your repository.
Key idea: GitHub Actions = “automation as code” inside your GitHub repository.
You describe workflows in .yml files, and GitHub runs them on events like push, pull_request, or release.
Core Concepts: Workflows, Jobs, Steps
Workflows
A workflow is a full automation process, defined in a YAML file inside .github/workflows/.
Example: .github/workflows/ci.yml
name: CI
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: echo "Running tests..."Important things:
name: Just a human‑readable name in the GitHub UI.on: Which events trigger this workflow, for examplepush,pull_request,workflow_dispatch(manual).jobs: One or more jobs that run in parallel by default.
Common triggers for backend projects
| Trigger | When it runs | Example use |
|---|---|---|
push | Any push to repo or to selected branches | Run unit tests, lint |
pull_request | When a PR is opened, synced, or reopened | Validate PR before merge |
workflow_dispatch | Manually triggered from GitHub UI | Manual deploy |
release | When a GitHub Release is created or published | Build and publish Docker artifacts |
schedule | At fixed times via cron syntax, e.g. nightly | Nightly tests, cleanup tasks |
Example: run only on pushes to main:
on:
push:
branches: [ main ]Jobs
A job runs on one virtual machine (runner). Jobs in a workflow are independent unless you define dependencies.
jobs:
test:
runs-on: ubuntu-latest
steps: ...
build:
runs-on: ubuntu-latest
needs: test
steps: ...runs-on: Which OS the job uses, for exampleubuntu-latest,windows-latest,macos-latest.needs: Makesbuildwait fortestto finish successfully.
This is useful for backend CI:
testjob: run tests and linters.buildjob: only run if tests pass, build Docker image or artifacts.deployjob: only run if build passes and branch ismain.
Steps
Each job has steps, which run sequentially inside that job.
Types of steps:
- Use an existing action with
uses - Run shell commands with
run
Example:
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run tests
run: pytest- If any step returns a non‑zero exit code, the job fails.
- Later steps in the same job do not run after a failure (unless you use advanced options like
continue-on-error).
Actions and Runners
What is an Action?
An action is a reusable piece of logic you can include in your workflow. It can be:
- Published by GitHub (for example
actions/checkout,actions/setup-python). - Published by the community (for example Docker login, deployment actions).
- Written by you specifically for your project.
You include them with uses:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: docker/build-push-action@v6You can search actions in the Marketplace on GitHub.
Typical actions used in backend CI
| Action | Purpose |
|---|---|
actions/checkout | Clone your repository into the runner |
actions/setup-python | Install and configure Python |
actions/cache | Cache dependencies to speed up builds |
docker/login-action | Log in to a container registry |
docker/build-push-action | Build and push Docker images |
What is a Runner?
A runner is the machine that executes your jobs.
Types:
- GitHub‑hosted runners
Provided by GitHub, for exampleubuntu-latest. You pay via GitHub Actions minutes (for public repos many minutes are free). - Self‑hosted runners
You run your own VM or server, and link it to GitHub. Useful if you need specific tools or want more control.
In most beginner backend projects, GitHub‑hosted ubuntu-latest is enough.
Example:
jobs:
test:
runs-on: ubuntu-latestCreating Your First Workflow
Minimal “Hello World” Workflow
Create a file .github/workflows/hello.yml:
name: Hello
on: [push]
jobs:
say-hello:
runs-on: ubuntu-latest
steps:
- name: Print a message
run: echo "Hello from GitHub Actions!"What happens?
- Whenever you push, GitHub triggers the
Helloworkflow. - It runs the
say-hellojob on Ubuntu. - That job has one step that prints a message.
You can see the results in the “Actions” tab in your repository.
Simple Python Backend CI Workflow
Now a more realistic example for a Python backend that uses requirements.txt and pytest.
.github/workflows/ci.yml:
name: CI
on:
push:
branches: [ main, dev ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run tests
run: pytestWhat this does in plain words:
- Triggered on:
- Pushes to
mainordev. - Pull requests targeting
main. - One
testjob: - Checks out your code.
- Installs Python 3.12.
- Installs dependencies.
- Runs the tests.
If any test fails, the workflow fails, and you will see a red “X” on the commit or PR.
Example: CI for a FastAPI Project
Assume you have a FastAPI project with:
requirements.txt- Tests in
tests/usingpytestandhttpx.
You want:
- Linting with
ruff. - Type checking with
mypy. - Tests with
pytest.
You can create .github/workflows/fastapi-ci.yml:
name: FastAPI CI
on:
push:
branches: [ main, dev ]
pull_request:
branches: [ main, dev ]
jobs:
lint-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install ruff mypy
- name: Lint with ruff
run: ruff check .
- name: Type check with mypy
run: mypy .
- name: Run tests
run: pytestKey points:
- All checks run in a single job and will run in order.
- If linting fails, type checking and tests do not run.
- This guarantees code quality before merge.
Building and Pushing Docker Images
Most backend deployments use Docker. GitHub Actions can build and push your Docker image automatically.
Example: Build and Push to Docker Hub
Assume:
- Docker Hub username:
mydockeruser - Repository:
mydockeruser/my-backend - You set two GitHub secrets:
DOCKERHUB_USERNAMEDOCKERHUB_TOKEN(an access token or password)
Workflow .github/workflows/docker.yml:
name: Build and Push Docker
on:
push:
branches: [ main ]
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
tags: |
mydockeruser/my-backend:latest
mydockeruser/my-backend:${{ github.sha }}What it does:
- On push to
main, it: - Checks out the code.
- Logs into Docker Hub using secrets.
- Builds the Docker image using your
Dockerfile. - Pushes two tags:
latest- The commit SHA, for example
c2f8a1d.
Using Secrets Safely
In CI/CD, you often need secrets:
- API keys.
- Database passwords.
- SSH keys.
- Docker registry tokens.
Never hardcode secrets in your workflows or source code.
Use GitHub Secrets instead.
Adding a Secret
- Go to your GitHub repository.
- Click “Settings” > “Secrets and variables” > “Actions”.
- Click “New repository secret”.
- Name it, for example
PRODUCTION_DB_PASSWORD. - Paste the value.
You can access it in workflows with ${{ secrets.NAME }}.
Example:
- name: Connect to database
run: |
echo "Using DB password, but not printing it"
env:
DB_PASSWORD: ${{ secrets.PRODUCTION_DB_PASSWORD }}
Here, DB_PASSWORD will be available inside the step as an environment variable but will not be printed. GitHub also masks secrets in logs.
Important:
Never echo your secrets directly. For example, echo $DB_PASSWORD will show it in logs. Avoid logging secrets at all times.
Conditional Jobs and Deployments
You usually want to run deployments only from certain branches, for example main or tags.
GitHub Actions supports if: conditions.
Example: Deploy Only from `main`
jobs:
deploy:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
# deployment steps hereYou can also combine conditions:
if: github.ref == 'refs/heads/main' && job.status == 'success'
Useful values in github context:
| Expression | Meaning |
|---|---|
github.ref | Ref string, e.g. refs/heads/main |
github.event_name | Event type, e.g. push, pull_request |
github.sha | Commit SHA |
Example: Simple SSH Deployment
For a small backend app on a Linux server, you might want to:
- Build a new Docker image.
- SSH into the server.
- Pull the new image.
- Restart the container.
Assume:
- You have a server accessible with SSH.
- You added:
DEPLOY_HOST(for exampleyour-server.com)DEPLOY_USER(for exampleubuntu)SSH_PRIVATE_KEY- Docker is configured on the server.
Workflow .github/workflows/deploy.yml:
name: Deploy
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up SSH
uses: webfactory/ssh-agent@v0.8.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Deploy to server
run: |
ssh -o StrictHostKeyChecking=no ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} << 'EOF'
cd /opt/my-backend
git pull origin main
docker compose pull
docker compose up -d
EOFThis is a simplified example, but it shows the pattern:
- Trigger on push to
main. - Start an SSH agent using a secret key.
- SSH into your server and run deployment commands.
Using Caching to Speed Up Workflows
Dependency installation can be slow. GitHub Actions provides actions/cache to cache files between runs.
Example: Cache Python Dependencies
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Cache pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run tests
run: pytestHow it works:
keyuses a hash ofrequirements.txt.- If
requirements.txtdid not change, the cache is reused andpip installis much faster. - If it changed,
hashFiles('requirements.txt')changes, and GitHub creates a new cache.
Multi‑Job Pipelines for Backend CI/CD
To combine everything, you can create a multi‑job pipeline:
testjob: run unit tests.buildjob: build and push Docker image, depends ontest.deployjob: deploy to server, depends onbuild.
Example .github/workflows/pipeline.yml:
name: Backend Pipeline
on:
push:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run tests
run: pytest
build:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
push: true
tags: mydockeruser/my-backend:${{ github.sha }}
deploy:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'
steps:
- name: Set up SSH
uses: webfactory/ssh-agent@v0.8.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Deploy
run: |
ssh -o StrictHostKeyChecking=no ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} << 'EOF'
cd /opt/my-backend
docker pull mydockeruser/my-backend:${{ github.sha }}
docker stop backend || true
docker rm backend || true
docker run -d --name backend -p 80:8000 mydockeruser/my-backend:${{ github.sha }}
EOFFlow:
testruns first.- If tests pass,
buildruns and pushes a Docker image for this commit. - If build succeeds,
deployruns and uses that image on the server.
This is a simple end‑to‑end CI/CD pipeline for a backend service.
Summary
- Workflows are YAML files in
.github/workflows/that describe automation. - Jobs run on runners, and steps run inside jobs.
- Use actions like
actions/checkout,actions/setup-python, and Docker actions to simplify CI. - Use secrets for any sensitive data, never hardcode them.
- Use caching to speed up installing dependencies.
- Build multi‑job pipelines where:
- Tests run first.
- Builds and Docker images come after.
- Deployment is the final job, only for trusted branches.
With these patterns, you can build reliable CI/CD pipelines for your backend projects directly in GitHub.
Views: 8
KAHIBARO