A detailed account of building a production-ready GitOps workflow - from containerizing apps to automated deployments with Flux CD, including all the debugging, fixes, and lessons learned.
This article documents the complete journey of setting up an automated GitOps pipeline, including every roadblock encountered and how they were resolved.
Table of Contents
- Introduction
- The Architecture
- Phase 1: App Containerization
- Phase 2: Setting Up the GitOps Repo
- Phase 3: Building the CI/CD Pipeline
- Phase 4: Debugging and Fixing
- Phase 5: Flux CD Integration
- The Complete Workflow
- Lessons Learned
- Resources
Introduction
Modern app deployment requires automation, reliability, and declarative config. GitOps provides all three by treating Git as the single source of truth for infrastructure and app state. This post documents building a complete GitOps pipeline from scratch for a Python study app.
GitHub Repos:
- App Code: github.com/t12-pybash/devops-study-app
- GitOps Manifests: github.com/t12-pybash/devops-study-app-gitops
All code examples in this post can be found in these repos. Feel free to explore, fork, and adapt for your own projects.
The App: A full-stack Python study tracker app consisting of:
- Backend: FastAPI REST API for managing study sessions
- Frontend: Flask web app for the user interface
- Both services containerized and deployed to Kubernetes
What we built:
- Automated Docker image builds
- GitOps repo for Kubernetes manifests (separate repo:
devops-study-app-gitops) - GitHub Actions CI/CD pipeline
- Flux CD for continuous deployment
- Automated image tag updates
- Pull request workflow for production changes
Development Tools Used:
- uv: Ultra-fast Python package installer (10x faster than pip)
- mise: Development env and tool version manager
- commitizen: Standardized git commit messages with semantic versioning
- k3d: Lightweight Kubernetes in Docker for local development
- pre-commit: Automated code quality checks (ruff, ruff-format)
- GitHub Copilot: Automated PR summaries and code reviews
The Challenge: Starting with a basic Python app, we needed to:
- Containerize frontend and backend services
- Structure Kubernetes manifests using Kustomize
- Set up local k3d cluster for testing
- Automate the entire deployment pipeline
- Handle multiple envs (dev/prod)
- Maintain separate repos for app code and GitOps manifests
- Debug numerous workflow issues
The Architecture
Components Overview
┌─────────────────────────────────────────────────────────────┐
│ Developer Workflow │
└─────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────┐
│ Push Git Tag │
│ backend-v0.0.5 │
└──────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ App Repo │
│ github.com/username/devops-study-app │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ GitHub Actions Workflow │ │
│ │ │ │
│ │ 1. Build Docker Image │ │
│ │ 2. Push to GitHub Container Registry (GHCR) │ │
│ │ 3. Trigger GitOps Update Workflow │ │
│ └────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────┐
│ Docker Image │
│ ghcr.io/user/app │
│ :backend-v0.0.5 │
└──────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ GitOps Repo │
│ github.com/username/devops-study-app-gitops │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ Automated Update Workflow │ │
│ │ │ │
│ │ 1. Clone GitOps repo │ │
│ │ 2. Update kustomization.yaml with new tag │ │
│ │ 3. Create Pull Request │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ apps/ │
│ ├── dev/kustomization.yaml │
│ └── prod/kustomization.yaml │
└─────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────┐
│ Merge PR │
│ (Manual Review) │
└──────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Kubernetes Cluster │
│ (k3d-study-app-cluster) │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ Flux CD │ │
│ │ │ │
│ │ 1. Monitors GitOps repo every 1 minute │ │
│ │ 2. Detects changes in kustomization.yaml │ │
│ │ 3. Applies changes to cluster │ │
│ │ 4. Rolling update with zero downtime │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ Namespaces: │
│ ├── study-app (dev) │
│ │ ├── dev-backend-pod (backend-v0.0.5) │
│ │ └── dev-frontend-pod (frontend-v0.2.0) │
│ └── study-app (prod) │
│ ├── prod-backend-pod (backend-v0.0.5) │
│ └── prod-frontend-pod (frontend-v0.2.0) │
└─────────────────────────────────────────────────────────────┘
Repo Structure
Two Separate Repos:
This setup follows GitOps best practices by separating app code from deployment manifests:
- App Repo (
devops-study-app): Contains source code, Dockerfiles, and CI/CD workflows - GitOps Repo (
devops-study-app-gitops): Contains Kubernetes manifests and Flux config
App Repo Structure:
devops-study-app/
├── .github/
│ └── workflows/
│ ├── docker-build-push.yaml # Main build workflow
│ ├── update-gitops.yaml # GitOps update workflow
│ ├── backend-tests.yaml # Backend testing
│ ├── frontend-tests.yaml # Frontend testing
│ ├── e2e-tests.yaml # End-to-end tests
│ └── copilot-code-review.yaml # Automated PR reviews with Copilot
├── src/
│ ├── backend/
│ │ ├── Dockerfile # Multi-stage Python build
│ │ ├── pyproject.toml # Backend dependencies (uv)
│ │ ├── uv.lock # Lockfile for reproducible builds
│ │ └── src/backend/main.py # FastAPI app
│ └── frontend/
│ ├── Dockerfile # Multi-stage Flask build
│ ├── pyproject.toml # Frontend dependencies (uv)
│ ├── uv.lock # Lockfile for reproducible builds
│ └── app.py # Flask app
├── scripts/
│ └── update_kustomize_tag # Shell script for tag updates
├── kubernetes/
│ └── k3d-config.yaml # Local cluster config
├── mise.toml # Tool version management
├── .pre-commit-config.yaml # Pre-commit hooks (ruff, commitizen)
└── .release-please-config.json # Automated versioning
GitOps Repo Structure:
devops-study-app-gitops/
├── apps/
│ ├── base/ # Base Kustomize configs
│ │ ├── backend/
│ │ │ ├── deployment.yaml
│ │ │ ├── service.yaml
│ │ │ └── kustomization.yaml
│ │ └── frontend/
│ │ ├── deployment.yaml
│ │ ├── service.yaml
│ │ └── kustomization.yaml
│ ├── dev/ # Dev overlays
│ │ ├── kustomization.yaml # Dev-specific config
│ │ ├── namespace.yaml
│ │ └── backend/kustomization.yaml
│ └── prod/ # Prod overlays
│ ├── kustomization.yaml # Prod-specific config
│ ├── namespace.yaml
│ └── backend/kustomization.yaml
└── clusters/
└── dev/
├── flux-system/ # Flux config
│ ├── gotk-components.yaml
│ ├── gotk-sync.yaml
│ └── kustomization.yaml
└── apps.yaml # App deployment config
Phase 1: App Containerization
Backend Dockerization (FastAPI + Python)
The backend is a FastAPI app using Python 3.13. We used a multi-stage build with uv for fast dependency installation.
Why uv?
- 10x faster than pip for dependency resolution
- Drop-in replacement for pip/pip-tools/poetry
- Deterministic builds with
uv.lock - Excellent Docker layer caching support
See the complete implementation:
Backend Dockerfile (excerpt):
| |
Key optimizations:
- Multi-stage build reduces final image size
- Layer caching for dependencies (only rebuilds when dependencies change)
- Non-root user for security
- Health checks for Kubernetes readiness probes
uvfor fast dependency resolution (10x faster than pip)
Frontend Dockerization (Flask + Python)
Similar approach for the Flask frontend.
See the complete implementation:
Frontend Dockerfile (excerpt):
| |
Local Testing with k3d
Before pushing to CI, test locally using k3d - a lightweight Kubernetes distribution that runs in Docker.
Why k3d?
- Fast cluster creation (~30 seconds)
- Minimal resource usage
- Perfect for CI/CD pipelines
- Compatible with standard kubectl commands
Set up local k3d cluster:
| |
k3d Config (kubernetes/k3d-config.yaml):
| |
Phase 2: Setting Up the GitOps Repo
Kustomize Structure
Kustomize allows us to define base configs and env-specific overlays without duplicating YAML.
See the complete manifests:
Base Backend Deployment (excerpt):
| |
Base Backend Service:
| |
Base Kustomization:
| |
Env Overlays
Dev Env:
| |
Prod Env:
| |
Testing Kustomize Locally
| |
Testing the complete flow locally:
| |
Phase 3: Building the CI/CD Pipeline
Docker Build and Push Workflow
See the complete workflows:
Main Workflow (excerpt):
| |
Phase 4: Debugging and Fixing
Issue 1: Wrong GitOps Repo Reference
Problem:
| |
Symptom: GitOps workflow succeeded but changes appeared in the wrong repo.
Fix:
| |
Lesson: Always verify repo references match your actual repo names.
Issue 2: Output Variable Name Mismatch
Problem:
| |
Symptom: GitOps PR created with empty tag:
| |
Fix:
| |
Lesson: Output variable names must match exactly between job definition and usage.
Issue 3: Workflow File Extension Mismatch
Problem:
| |
But actual file was named .yaml
Fix:
| |
Lesson: Be consistent with file extensions throughout the project.
Issue 4: Implementing GitHub Copilot PR Automation
Goal: Automate PR summaries and code reviews using GitHub Copilot.
Implementation:
Copilot PR Review Workflow (.github/workflows/copilot-code-review.yaml):
| |
Benefits:
- Automatically generates PR descriptions from commits
- Performs automated code review on every PR
- Identifies potential bugs and security issues
- Suggests improvements and best practices
- Reduces manual review time
Example Copilot PR Comment:
GitHub Copilot Review
Summary:
- Updated backend image tag from v0.0.4 to v0.0.5
- Changes affect production kustomization.yaml
- No breaking changes detected
Code Quality:
Image tag format follows semantic versioning
Kustomization syntax is valid
Consider adding resource limits in production
Suggestions:
- Add rollback strategy documentation
- Update changelog with deployment notes
Lesson: AI-powered code review catches issues early and improves PR quality without manual overhead.
Phase 5: Flux CD Integration
Installing Flux
Bootstrap Flux to your cluster:
| |
Verifying Flux
| |
The Complete Workflow
End-to-End Flow
- Developer makes changes using commitizen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14# Make code changes vim src/backend/main.py # Stage changes git add . # Commit using commitizen (ensures semantic versioning) cz commit # Or use the commitizen CLI interactively git commit # Pre-commit hook will validate commit message # Tag the release (semantic versioning) git tag backend-v0.0.5 git push origin backend-v0.0.5
Commitizen ensures standardized commits:
feat(backend): add new study session endpoint
fix(frontend): resolve login page styling issue
docs: update API documentation
chore: bump dependencies to latest versions
GitHub Actions builds image
- Output:
ghcr.io/username/study-app-api:backend-v0.0.5
- Output:
GitOps workflow updates manifests
- Updates dev (direct push)
- Creates PR for prod
- GitHub Copilot automatically generates PR summary
- Copilot performs automated code review
Review PR with AI assistance:
1 2 3 4 5 6 7 8# View PR with Copilot-generated summary gh pr view 2 --repo username/devops-study-app-gitops # Copilot provides: # - Automated change summary # - Code review comments # - Potential issues flagged # - Suggested improvementsMerge PR:
1gh pr merge 2 --repo username/devops-study-app-gitops --squashFlux deploys automatically
- Detects new commit
- Pulls new image
- Rolling update with zero downtime
Verify deployment:
1 2kubectl get pods -n study-app kubectl describe pod dev-backend-xxx -n study-app | grep Image:
Lessons Learned
Key Takeaways
- Separate repos for code and manifests - App repo and GitOps repo provide clear separation of concerns
- GitOps provides declarative infrastructure - Git is the single source of truth
- Automation reduces human error - Tag push → Deploy automatically
- Pull requests enable review - Production changes require approval
- Flux handles reconciliation - Continuous synchronization
- Multi-stage builds optimize images - Smaller, faster, more secure
- Kustomize reduces duplication - Base + overlays pattern scales well
- k3d enables local testing - Fast, lightweight Kubernetes for development
- uv accelerates Python builds - 10x faster than pip, perfect for CI/CD
- mise manages tool versions - Consistent envs across team
- commitizen enforces standards - Semantic versioning and clean git history
- GitHub Copilot reviews code - Automated PR summaries and reviews catch issues early
- Proper testing prevents issues - Test early, test often
- Debugging is part of the process - Learn from errors and iterate
Resources
Project Repos
Live Implementation:
App Repo: github.com/t12-pybash/devops-study-app
- Source code (FastAPI backend, Flask frontend)
- Dockerfiles and build configs
- GitHub Actions CI/CD workflows
- Testing infrastructure (unit, integration, e2e)
GitOps Repo: github.com/t12-pybash/devops-study-app-gitops
- Kubernetes manifests (base + overlays)
- Flux CD config
- Kustomize configs for dev/prod
- Cluster bootstrapping
Explore the code, workflows, and configs. Star the repos if you find them helpful!
Documentation
- Flux CD Documentation
- Kustomize Documentation
- GitHub Actions Documentation
- uv Documentation
- k3d Documentation
- mise Documentation
Tools Used
Development Env:
- mise: Development env and tool version manager (replaces asdf/nvm)
- uv: Ultra-fast Python package installer (10-100x faster than pip)
- commitizen: Standardized git commits and semantic versioning
- pre-commit: Automated code quality checks (ruff, ruff-format)
- GitHub Copilot: AI-powered PR summaries and code reviews
Container & Orchestration:
- Docker: Container runtime
- k3d: Lightweight Kubernetes in Docker (perfect for local dev)
- Flux CD: GitOps continuous delivery operator
- Kustomize: Template-free Kubernetes config management
CI/CD:
- GitHub Actions: Workflow automation
- GitHub Container Registry (GHCR): Docker image hosting
- Release Please: Automated changelog and version management
Repo Structure:
- App Repo (
devops-study-app): Source code, Dockerfiles, CI/CD - GitOps Repo (
devops-study-app-gitops): Kubernetes manifests, Flux config
This post documents a real implementation journey, including all mistakes and solutions. GitOps is powerful, but requires understanding, patience, and iteration to get right.
Tags: #GitOps #Kubernetes #FluxCD #Docker #CICD #DevOps #Kustomize #Automation