AppFlow Tracker is an open-source Django + React application. Every code snippet and workflow file in this article is live and working in the repository. You can run the app right now — no setup required.
Before we talk about automation, let’s get the app running. You have two options.
You need Docker Desktop installed.
bash# 1. Pull both images from Docker Hubdocker pull kennjenga/appflow-tracker-backend:latestdocker pull kennjenga/appflow-tracker-frontend:latest
Then create a docker-compose.yml file anywhere on your machine:
yamlservices:backend:image: kennjenga/appflow-tracker-backend:latestenvironment:SECRET_KEY: local-demo-onlyDEBUG: "True"ALLOWED_HOSTS: localhost,127.0.0.1CORS_ALLOWED_ORIGINS: http://localhost,http://127.0.0.1DJANGO_DB_PATH: /app/db/db.sqlite3volumes:- sqlite_data:/app/dbexpose:- "8000"frontend:image: kennjenga/appflow-tracker-frontend:latestports:- "80:80"depends_on:- backendvolumes:sqlite_data:
Start the app:
bash# 2. Start the appdocker compose up# 3. Open your browser# App: http://localhost# API docs: http://localhost/api/v1/docs# Admin: http://localhost/admin
bash# 1. Clonegit clone https://github.com/Kennjenga/appflow-tracker.gitcd appflow-tracker# 2. Copy the Docker env filecp .env.docker.example .env.docker# 3. Build and start (first run downloads base images + installs deps)docker compose up --build# 4. Create an admin user (first run only)docker compose exec backend python manage.py createsuperuser
Same URLs apply: http://localhost for the app, and http://localhost/admin for the Django admin.
Docker Compose built two containers from the source code:
It also connected them on a private network.
The GitHub Actions workflows we’re about to study are what built those Docker images and pushed them to Docker Hub automatically.
GitHub Actions is an automation platform built directly into GitHub.
You write a YAML file, put it in a special folder in your repository, and GitHub automatically runs it whenever something happens:
No extra accounts. No separate CI/CD server. It’s already there.
javascriptyour-project/├── src/├── tests/└── .github/└── workflows/├── pr-tests.yml ← runs on every Pull Request└── docker-publish.yml ← runs on every push to master
Two YAML files can be enough for two fully automated pipelines.
❌ Unsupported block (table)Everything in GitHub Actions is built from four building blocks:
EVENT → WORKFLOW → JOB → STEP
An event is what kicks the workflow off. You choose which events to listen for.
yamlon:push: # Code was pushed to the repobranches:- masterpull_request: # A PR was opened or updatedbranches:- master- stagingschedule: # Runs on a timer (cron syntax)- cron: '0 2 * * *' # Every day at 2 AM UTCworkflow_dispatch: # Manual "Run workflow" button in GitHub UI
One YAML file equals one workflow. A workflow contains one or more jobs.
Each job runs on its own fresh virtual machine (a Runner). Jobs run in parallel by default. Use needs: to chain them.
yamljobs:test:runs-on: ubuntu-lateststeps: [...]build:runs-on: ubuntu-latestneeds: teststeps: [...]deploy:runs-on: ubuntu-latestneeds: buildsteps: [...]
A step is one unit of work inside a job.
yamlsteps:- name: Checkout codeuses: actions/checkout@v4- name: Install dependenciesrun: pip install -r requirements.txt- name: Run testsrun: pytestenv:SECRET_KEY: $ secrets.SECRET_KEY
File: .github/workflows/pr-tests.yml
Purpose: Every time a developer opens or updates a Pull Request targeting master or staging, this pipeline automatically runs the full test suite. If any test fails, the PR is blocked from merging.
This is Continuous Integration (CI): automatically validating every code change before it touches a protected branch.
yaml# ════════════════════════════════════════════════════════════════════════════# CI Pipeline — Runs on every Pull Request targeting master or staging## What this pipeline does:# Job 1 (backend-tests) → Install Python deps → Run pytest# Job 2 (frontend-tests) → Install Node deps → Run Vitest → Vite build## Both jobs run in PARALLEL (no `needs:` between them).# The PR is only considered green when BOTH jobs pass. ✅# ════════════════════════════════════════════════════════════════════════════name: PR Tests# ── TRIGGER ─────────────────────────────────────────────────────────────────on:pull_request:branches:- master- staging# ── JOBS ─────────────────────────────────────────────────────────────────────jobs:backend-tests:name: 🐍 Backend — pytestruns-on: ubuntu-latestdefaults:run:working-directory: backendsteps:- name: Checkout repositoryuses: actions/checkout@v4- name: Set up Python 3.12uses: actions/setup-python@v5with:python-version: '3.12'- name: Cache pip dependenciesuses: actions/cache@v4with:path: ~/.cache/pipkey: pip-$ runner.os -$ hashFiles('**/requirements.txt')restore-keys: |pip-$ runner.os -- name: Install dependenciesrun: pip install -r requirements.txt- name: Django system checkrun: python manage.py checkenv:SECRET_KEY: ci-only-not-a-real-secretDEBUG: "True"- name: Run pytestrun: pytest --tb=short -venv:SECRET_KEY: ci-only-not-a-real-secretDEBUG: "True"frontend-tests:name: ⚛️ Frontend — Vitest + Buildruns-on: ubuntu-latestdefaults:run:working-directory: frontendsteps:- name: Checkout repositoryuses: actions/checkout@v4- name: Set up Node.js 20uses: actions/setup-node@v4with:node-version: '20'- name: Cache node_modulesuses: actions/cache@v4with:path: frontend/node_moduleskey: node-$ runner.os -$ hashFiles('**/package-lock.json')restore-keys: |node-$ runner.os -- name: Install dependenciesrun: npm ci- name: Run Vitest testsrun: npm run test- name: Production build checkrun: npm run buildenv:VITE_API_BASE_URL: http://localhost:8000/api/v1
javascriptDeveloper opens a PR → master or staging↓[Event: pull_request fires]↓┌───────────────────┐ ┌───────────────────────┐│ 🐍 backend-tests │ │ ⚛️ frontend-tests │ ← run in PARALLEL│ 1. Checkout code │ │ 1. Checkout code ││ 2. Setup Python │ │ 2. Setup Node 20 ││ 3. Cache pip │ │ 3. Cache node_modules ││ 4. pip install │ │ 4. npm ci ││ 5. Django check │ │ 5. npm run test ││ 6. pytest │ │ 6. npm run build │└────────┬──────────┘ └──────────┬────────────┘│ both must pass ✅ │└──────────┬──────────────┘↓PR gets a green checkmark → team can safely merge
File: .github/workflows/docker-publish.yml
Purpose: Every time code is merged into master, this pipeline builds Docker images for the backend and frontend, and pushes them to Docker Hub.
This is Continuous Deployment (CD): automatically shipping a new version after every passing merge.
yamlname: Build & Push to Docker Hubon:push:branches:- masterworkflow_dispatch:env:REGISTRY: docker.ioBACKEND_IMAGE: kennjenga/appflow-tracker-backendFRONTEND_IMAGE: kennjenga/appflow-tracker-frontendjobs:build-backend:name: Backend — Build & Pushruns-on: ubuntu-latestpermissions:contents: readsteps:- name: Checkout repositoryuses: actions/checkout@v4- name: Set up Docker Buildxuses: docker/setup-buildx-action@v3- name: Log in to Docker Hubuses: docker/login-action@v3with:username: $ secrets.DOCKERHUB_USERNAMEpassword: $ secrets.DOCKERHUB_TOKEN- name: Extract metadata (tags & labels)id: meta-backenduses: docker/metadata-action@v5with:images: $ env.BACKEND_IMAGEtags: |type=raw,value=latest,enable=is_default_branchtype=sha,prefix=sha-,format=short- name: Build and push backend imageuses: docker/build-push-action@v6with:context: ./backendfile: ./backend/Dockerfilepush: truetags: $ steps.meta-backend.outputs.tagslabels: $ steps.meta-backend.outputs.labelscache-from: type=gha,scope=backendcache-to: type=gha,mode=max,scope=backendplatforms: linux/amd64,linux/arm64build-frontend:name: Frontend — Build & Pushruns-on: ubuntu-latestpermissions:contents: readsteps:- name: Checkout repositoryuses: actions/checkout@v4- name: Set up Docker Buildxuses: docker/setup-buildx-action@v3- name: Log in to Docker Hubuses: docker/login-action@v3with:username: $ secrets.DOCKERHUB_USERNAMEpassword: $ secrets.DOCKERHUB_TOKEN- name: Extract metadata (tags & labels)id: meta-frontenduses: docker/metadata-action@v5with:images: $ env.FRONTEND_IMAGEtags: |type=raw,value=latest,enable=is_default_branchtype=sha,prefix=sha-,format=short- name: Build and push frontend imageuses: docker/build-push-action@v6with:context: ./frontendfile: ./frontend/Dockerfilepush: truetags: $ steps.meta-frontend.outputs.tagslabels: $ steps.meta-frontend.outputs.labelsbuild-args: |VITE_API_BASE_URL=/api/v1cache-from: type=gha,scope=frontendcache-to: type=gha,mode=max,scope=frontendplatforms: linux/amd64,linux/arm64
javascriptDeveloper merges PR → master↓[Event: push to master fires]↓┌──────────────────────────┐ ┌───────────────────────────┐│ build-backend │ │ build-frontend │ ← PARALLEL│ 1. Checkout │ │ 1. Checkout ││ 2. Setup Buildx │ │ 2. Setup Buildx ││ 3. Login to Docker Hub │ │ 3. Login to Docker Hub ││ 4. Generate :latest tag │ │ 4. Generate :latest tag ││ and :sha-abc1234 tag │ │ and :sha-abc1234 tag ││ 5. Build + Push image │ │ 5. Build + Push image │└──────────────────────────┘ └───────────────────────────┘↓Images are updated on Docker Hub
Notice this pattern in the CD workflow:
yamlusername: $ secrets.DOCKERHUB_USERNAMEpassword: $ secrets.DOCKERHUB_TOKEN
Secrets are encrypted key-value pairs stored in GitHub, never in your code.
GitHub masks secrets in logs. Even if a step prints a secret, it will show as ***.
Without caching, every pipeline run downloads all dependencies from scratch. For a Python + Node project this can add minutes per run. Caching can cut repeat runs down dramatically.
yaml- name: Cache pip dependenciesuses: actions/cache@v4with:path: ~/.cache/pipkey: pip-$ runner.os -$ hashFiles('**/requirements.txt')restore-keys: |pip-$ runner.os -
yaml- name: Cache node_modulesuses: actions/cache@v4with:path: frontend/node_moduleskey: node-$ runner.os -$ hashFiles('**/package-lock.json')restore-keys: |node-$ runner.os -
Together, these two workflows create a complete automated software delivery system.
javascript┌─────────────────────────────────────────────────────────────────┐│ DEVELOPMENT LIFECYCLE ││ ││ 1. Developer writes code on a feature branch ││ ↓ ││ 2. Developer opens a Pull Request → master (or staging) ││ ↓ ││ ┌─────────────────────────────┐ ││ │ pr-tests.yml fires │ ← CI ││ │ 🐍 Backend tests (pytest) │ ││ │ ⚛️ Frontend tests (Vitest)│ ││ │ ⚛️ Vite production build │ ││ └──────────────┬──────────────┘ ││ │ ││ ✅ all pass ││ ↓ ││ 3. Team reviews and approves the PR ││ ↓ ││ 4. PR is merged into master ││ ↓ ││ ┌─────────────────────────────┐ ││ │ docker-publish.yml fires │ ← CD ││ │ 🐳 Build backend image │ ││ │ 🐳 Build frontend image │ ││ │ 📦 Push → Docker Hub │ ││ └──────────────┬──────────────┘ ││ ↓ ││ 5. Anyone can pull the new image ││ docker pull kennjenga/appflow-tracker-backend:latest ││ │└─────────────────────────────────────────────────────────────────┘
yaml$ secrets.MY_SECRET$ env.MY_ENV_VAR$ github.sha$ github.ref$ github.actor$ runner.os$ hashFiles('path/to/file')$ steps.step_id.outputs.some_output
yamluses: actions/checkout@v4uses: actions/setup-python@v5uses: actions/setup-node@v4uses: actions/cache@v4uses: actions/upload-artifact@v4uses: docker/setup-buildx-action@v3uses: docker/login-action@v3uses: docker/build-push-action@v6uses: docker/metadata-action@v5
yamlon:push:branches: [master, staging]pull_request:branches: [master, staging]schedule:- cron: '0 2 * * 1'workflow_dispatch:release:types: [published]
The full source code for AppFlow Tracker, including both workflow files, is available at https://github.com/Kennjenga/appflow-tracker .
The Docker images are public at https://hub.docker.com/u/kennjenga .
