Every DevOps engineer talks about CI/CD. Fewer actually build one end-to-end from scratch. This project is a complete CI/CD pipeline that takes code from a Git push, builds a Docker image, runs tests, pushes to a registry, and deploys to a server — fully automated, zero manual steps.
The Problem CI/CD Solves
Without a pipeline, deployment looks like this: developer finishes code, SSHes into the server, pulls the repo, manually builds the image, restarts the container, hopes nothing breaks. It works once. It breaks the tenth time when someone forgets a step, deploys the wrong branch, or pushes untested code.
A CI/CD pipeline removes humans from the deployment path. Code that passes tests ships automatically. Code that fails never touches production.
Pipeline Architecture
GitHub Actions Workflow
name: CI/CD Pipeline
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run tests
run: |
pip install -r requirements.txt
pytest tests/ -v
build-and-deploy:
needs: test
runs-on: ubuntu-latest
steps:
- name: Build Docker image
run: docker build -t ${{ secrets.REGISTRY }}/app:${{ github.sha }} .
- name: Push to registry
run: |
echo ${{ secrets.REGISTRY_TOKEN }} | docker login -u ${{ secrets.REGISTRY_USER }} --password-stdin
docker push ${{ secrets.REGISTRY }}/app:${{ github.sha }}
- name: Deploy to server
uses: appleboy/ssh-action@v0.1.10
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
docker pull ${{ secrets.REGISTRY }}/app:${{ github.sha }}
docker compose up -d --no-deps app
Secrets Management
No credentials in code. Ever. All sensitive values — registry tokens, SSH keys, server addresses — live in GitHub Actions Secrets and are injected at runtime as environment variables. The workflow file in the repo contains zero plaintext secrets.
Docker Image Strategy
Each build produces an image tagged with the Git commit SHA. This means every deployed version is traceable back to an exact commit — rollback is as simple as deploying a previous tag.
# Multi-stage build — keeps final image lean
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user -r requirements.txt
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0"]
Key Lessons
- Tag images with commit SHAs, not
latest—latestmakes rollback impossible - Test in CI before building — fail fast, before wasting time on an image build
- Use multi-stage Docker builds — production images don't need build tools
- Store all secrets in GitHub Actions Secrets — never in
.envfiles committed to repos
What's Next
- Staging environment gate — deploy to staging, run integration tests, then promote to production
- Slack notifications on deploy success/failure
- Terraform to provision the deployment server automatically
- Kubernetes deployment replacing Docker Compose on the server