How to Set Up CI/CD for a Python Project Using GitHub Actions and Docker: A Step-by-Step Guide

Introduction

Python development isn't just about writing clean code—it's also a constant battle with routine: running tests, building the application, deploying to a server. Doing this manually every time wastes time and risks errors. CI/CD (Continuous Integration / Continuous Delivery) solves this problem once and for all. The combination of GitHub Actions and Docker allows you to automate the entire process: from pushing to the repository to running the container in production. In this article, as a practicing DevOps engineer, I'll show you how to set up a pipeline for a typical FastAPI backend. We'll break down configurations, best practices, and common mistakes—no fluff, just code and real experience.

What is CI/CD and Why Does a Python Developer Need It?

CI/CD is a methodology that turns scattered steps (linter, tests, build, deploy) into a single automated pipeline. For a Python project, this is especially relevant: you can instantly check whether a new commit breaks existing logic and deliver updates to users in minutes. GitHub Actions is a free built-in GitHub tool that runs your scripts in response to events (push, pull request). Docker, on the other hand, packages the application with all its dependencies, ensuring it runs identically on your laptop, test server, and AWS cloud.

Step-by-Step Guide: From Repository to Production

Step 1. Preparing a FastAPI Python Project

Let's assume we have a simple FastAPI application with one endpoint. File structure:

my-python-app/
├── app/
│   ├── __init__.py
│   └── main.py
├── requirements.txt
├── Dockerfile
├── .github/
│   └── workflows/
│       └── ci-cd.yml
└── tests/
    └── test_main.py

Contents of app/main.py:

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"message": "Hello, DevOps!"}

File requirements.txt:

fastapi==0.111.0
uvicorn==0.30.1
pytest==8.2.0

Step 2. Creating a Dockerfile

The Dockerfile is the recipe for building the image. For Python, we use the official lightweight image python:3.12-slim:

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Step 3. Setting Up GitHub Actions for CI (Testing and Linting)

Create the file .github/workflows/ci-cd.yml. Let's start with the continuous integration (CI) stage:

name: CI/CD Pipeline

on:
  push:
    branches: [main]
  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: |
          pytest

This pipeline runs on every push to main or pull request creation. It installs dependencies and runs tests. If tests fail, deployment won't happen.

Step 4. Adding Docker Image Build and Push (CD)

Now let's add the delivery (CD) stage. For this, we'll need Docker Hub (or GitHub Container Registry). Create secrets in the repository: Settings > Secrets and variables > Actions—add DOCKER_USERNAME and DOCKER_PASSWORD.

Extend ci-cd.yml:

  build-and-push:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ secrets.DOCKER_USERNAME }}/my-python-app:latest

Step 5. Automatic Deployment to AWS EC2

For full automation, add deployment to a virtual machine (e.g., AWS EC2). Use SSH:

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to EC2
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USER }}
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            docker pull ${{ secrets.DOCKER_USERNAME }}/my-python-app:latest
            docker stop my-app || true
            docker rm my-app || true
            docker run -d --name my-app -p 80:8000 ${{ secrets.DOCKER_USERNAME }}/my-python-app:latest

This step connects to EC2, pulls the fresh image, and restarts the container. In a real project, you should add health checks and rollback on error.

Table: CI/CD Stage Comparison

Stage Tool Action Criticality
Testing GitHub Actions (pytest) Run unit tests High—catches bugs before build
Image build Docker + GitHub Actions Create container Medium—packages code
Push to registry Docker Hub Store versions Medium—needed for deploy
Deploy to server SSH + Docker Run container High—delivers features

Common Mistakes and How to Avoid Them

  1. Secrets in code: Never store passwords or keys in YAML files. Use GitHub secrets. If a secret leaks—revoke it immediately.
  2. Heavy images: Use slim versions of Python and multi-stage builds—this speeds up deployment.
  3. Lack of tests: If tests aren't written, CI/CD loses its purpose. At least one test should exist.
  4. Ignoring caching: GitHub Actions caches dependencies via actions/cache, reducing pipeline time by 2-3 times.

LSI Keywords and Practical Recommendations

In the context of CI/CD for Python, it's important to consider the following related terms (LSI):
- containerization—the foundation of Docker, allows isolating the environment;
- orchestration—managing multiple containers (e.g., Kubernetes);
- monitoring—observing the running application (Prometheus + Grafana);
- infrastructure as code (IaC)—describing servers via Terraform or Ansible;
- logging—collecting logs for debugging (ELK stack);
- container security—scanning images for vulnerabilities (Trivy);
- deployment automation—the key goal of the entire pipeline.

I recommend adding a Docker image security scanning step to your pipeline—this protects against known CVEs. For example, use aquasecurity/trivy-action.

Conclusion

You've just set up a full CI/CD pipeline for a Python project: from testing to deployment on AWS EC2. GitHub Actions and Docker are a powerful duo that saves hours of manual work and reduces the risk of human error. Now your code is automatically checked, and users receive updates faster. If you want to dive deeper and learn to manage infrastructure at a professional level—study Kubernetes, Terraform, and monitoring. Start small: add dependency caching to your workflow and see how much faster the build becomes. Good luck with automation!

← All posts

Comments