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
- Secrets in code: Never store passwords or keys in YAML files. Use GitHub
secrets. If a secret leaks—revoke it immediately. - Heavy images: Use slim versions of Python and multi-stage builds—this speeds up deployment.
- Lack of tests: If tests aren't written, CI/CD loses its purpose. At least one test should exist.
- 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!
Comments