This project is a small C++ backend service used as a demo application for the Modern Practices in DevOps course final project.
The main focus is not on complex business logic, but on demonstrating a complete CI/CD pipeline, containerization, Kubernetes deployment, and basic security practices around the service.
The project demonstrates:
- Building and testing a C++ service with CMake
- Packaging the service as a Docker image
- Publishing images to GitHub Container Registry (GHCR)
- Implementing Continuous Integration (CI) with GitHub Actions:
- Build, unit tests, Docker build
- Implementing Continuous Delivery (CD) with GitHub Actions:
- Build & push Docker image
- Deploy to a Kubernetes cluster (ephemeral kind cluster in CI)
- Automatic
/healthsmoke test inside the cluster
- Applying key DevOps practices:
- Source control (Git + GitHub)
- Branching & pipelines
- Security / SAST (CodeQL analysis for C++)
- Infrastructure as Code (Kubernetes manifests)
Currently the backend exposes a single HTTP endpoint:
-
GET /health– returns a small JSON payload indicating that the service is alive, e.g.:{ "status": "ok", "service": "taskboard-backend" }
The implementation is intentionally minimal so that the main focus is on the DevOps pipeline and infrastructure (Docker, Kubernetes, CI/CD, security).
- Language: C++17
- Build system: CMake
- Tests: Custom test runner +
ctest - Containers: Docker (multi-stage build)
- Registry: GitHub Container Registry (GHCR)
- Kubernetes: kind (ephemeral cluster inside GitHub Actions) + K8s YAML manifests
- CI/CD: GitHub Actions (
.github/workflows/ci.yml,.github/workflows/cd.yml) - Security / SAST: GitHub CodeQL scanning for C++
High-level layout:
.
├─ backend/
│ ├─ include/
│ │ └─ http_responses.h # Common HTTP response helpers
│ ├─ src/
│ │ ├─ main.cpp # Socket-based HTTP server + /health
│ │ └─ http_responses.cpp # Implementation of HTTP responses
│ ├─ tests/
│ │ └─ http_responses_tests.cpp # Unit tests for HTTP response logic
│ └─ CMakeLists.txt
│
├─ k8s/
│ ├─ namespace.yaml # Namespace: taskboard
│ ├─ deployment.yaml # Deployment: taskboard-backend
│ └─ service.yaml # Service: taskboard-backend (ClusterIP)
│
├─ .github/
│ └─ workflows/
│ ├─ ci.yml # CI: build + tests + docker build
│ └─ cd.yml # CD: build + push + deploy to Kubernetes (kind)
│
├─ CMakeLists.txt # Root CMake project
├─ Dockerfile # Multi-stage Docker build
├─ .dockerignore # Ignore build artifacts, .git, etc.
└─ README.mdFrom the project root:
mkdir -p build
cd build
cmake ..
cmake --build . --config ReleaseThis produces (paths depend on your CMake generator):
backend/taskboard_backend– the server executablebackend/taskboard_tests– unit tests binary
Run the backend:
./backend/taskboard_backend
# The server listens on port 5000 by defaultTest the /health endpoint:
curl -v http://localhost:5000/healthYou should receive a JSON response similar to:
{"status":"ok","service":"taskboard-backend"}-
Open Visual Studio and use “Open a local folder” pointing to the repository root.
-
Visual Studio will detect the CMake project automatically.
-
Use Build → Build All (
Ctrl+Shift+B) to build:taskboard_corestatic librarytaskboard_backendexecutabletaskboard_testsexecutable
-
Select
taskboard_backendas the startup target and run (Ctrl+F5). -
Use
curlor a browser to callhttp://localhost:5000/health.
Unit tests are located in:
backend/tests/http_responses_tests.cpp
They validate:
-
Correct status lines:
HTTP/1.1 200 OKfor/healthHTTP/1.1 404 Not Foundfor unknown paths
-
Presence of
Content-Type: application/json -
Correct JSON bodies for the responses
Run tests locally via CTest:
cd build
ctest --output-on-failureThe CI pipeline (ci.yml) runs the same ctest command on every push / pull request.
The root Dockerfile is a multi-stage build:
-
Builder stage
- Base image:
debian:bookworm-slim - Installs
build-essentialandcmake - Configures and builds the C++ project with CMake
- Base image:
-
Runtime stage
-
Base image:
debian:bookworm-slim -
Copies only the compiled
taskboard_backendbinary from the builder stage -
Exposes port
5000 -
Starts the server via:
./taskboard_backend
-
If you have Docker installed locally, you can build and run the image:
docker build -t taskboard-backend:local .
docker run --rm -p 5000:5000 taskboard-backend:local
curl -v http://localhost:5000/healthKubernetes manifests are located in the k8s/ directory:
-
namespace.yaml– defines thetaskboardNamespace. -
deployment.yaml– defines aDeployment:- Name:
taskboard-backend - Namespace:
taskboard - 2 replicas
- Container image:
ghcr.io/johnfromspace/taskboard-backend:latest - Container port:
5000 readinessProbeandlivenessProbeusing the/healthendpoint
- Name:
-
service.yaml– defines aClusterIPService:- Name:
taskboard-backend - Namespace:
taskboard - Service port:
80 - Target port:
5000 - Selector:
app: taskboard-backend
- Name:
The CD pipeline uses kind (Kubernetes-in-Docker) inside GitHub Actions to create an ephemeral Kubernetes cluster for each run. The basic idea is:
- Create a kind cluster in the CI runner.
- Load the Docker image into the kind nodes.
- Apply the Kubernetes manifests.
- Wait for the rollout to finish.
- Port-forward the service and call
/health.
This provides a real Kubernetes deployment and smoke test without requiring a persistent external cluster or local Docker Desktop.
In addition to the CI/CD kind cluster, the same manifests can be tested against the local Kubernetes cluster provided by Docker Desktop.
- Enable Kubernetes in Docker Desktop
Open Docker Desktop → Settings → Kubernetes
Enable "Enable Kubernetes" and apply the changes.
After Kubernetes is running, verify:
kubectl config current-context
kubectl get nodesYou should see the docker-desktop context and a Ready node.
- Apply project manifests
From the repository root:
kubectl apply -f k8s/namespace.yaml
kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/service.yaml
kubectl get pods -n taskboard
kubectl get svc -n taskboardYou should see two taskboard-backend pods and a taskboard-backend service.
- Use the locally built image (
taskboard-backend:local)
If you want to run exactly the same image you built locally (with docker build -t taskboard-backend:local .), you can update the deployment to use it:
kubectl set image deployment/taskboard-backend \
taskboard-backend=taskboard-backend:local \
-n taskboardThen wait for the rollout:
kubectl rollout status deployment/taskboard-backend -n taskboard
kubectl get pods -n taskboard- Port-forward and test /health
Expose the service locally:
kubectl port-forward svc/taskboard-backend -n taskboard 5000:80Leave this command running, then in another terminal:
curl http://localhost:5000/healthExpected response:
{"status":"ok","service":"taskboard-backend"}This confirms that:
The application is running inside the local Kubernetes cluster.
The Service routes traffic correctly to the pods.
The /health endpoint is reachable through Kubernetes.
- Clean up (optional)
To remove the resources from the local cluster:
kubectl delete -f k8s/service.yaml
kubectl delete -f k8s/deployment.yaml
kubectl delete -f k8s/namespace.yaml
# or simply:
# kubectl delete namespace taskboardThe CI workflow is responsible for:
- Ensuring that the C++ code builds successfully.
- Running unit tests.
- Validating that the Dockerfile can build.
Key steps:
-
Checkout
- uses: actions/checkout@v4
-
Install build tools
- run: | sudo apt-get update sudo apt-get install -y --no-install-recommends \ build-essential \ cmake
-
Configure & build with CMake
- run: | mkdir -p build cd build cmake .. cmake --build . --config Release
-
Run tests
- run: | cd build ctest --output-on-failure
-
Docker build sanity check
- run: | docker build -t taskboard-backend:ci .
Permissions are restricted to the minimum needed:
permissions:
contents: readThe CD workflow handles:
- Building and pushing the Docker image to GHCR.
- Deploying the application to a Kubernetes cluster (kind) inside GitHub Actions.
- Running a smoke test against
/health.
High-level steps:
-
Checkout repository
-
Set up Docker Buildx
-
Log in to GHCR using
github.token -
Build & push Docker image
-
Image tag format:
ghcr.io/<owner>/taskboard-backend:{sha, latest}
-
-
Create a kind cluster using
helm/kind-action -
Load the image into kind to avoid pulling from the network
-
Apply Kubernetes manifests
kubectl apply -f k8s/namespace.yaml kubectl apply -f k8s/deployment.yaml kubectl apply -f k8s/service.yaml
-
Wait for rollout
kubectl rollout status deployment/taskboard-backend -n taskboard kubectl get pods -n taskboard
-
Smoke test
/healthendpointkubectl port-forward svc/taskboard-backend -n taskboard 5000:80 & curl -f http://localhost:5000/health
If any of these steps fail, the CD pipeline fails, ensuring that only healthy deployments are considered successful.
Permissions:
permissions:
contents: read
packages: write-
Static Analysis (SAST): GitHub CodeQL code scanning is enabled for this repository via the Security tab. It analyzes the C++ code for common vulnerabilities and code issues.
-
CI Hardening:
- Minimal GitHub Actions
permissionsin both CI and CD workflows (principle of least privilege). - Unit tests integrated into CI (pipeline fails if tests fail).
- Minimal GitHub Actions
-
Infrastructure as Code:
- Kubernetes manifests are stored in version control.
- CI/CD workflows are defined as code in
.github/workflows/.
The current version focuses on the DevOps side. Possible extensions:
- Implement a real TaskBoard API (tasks, statuses, priorities).
- Add a database (e.g. PostgreSQL) and migrations (e.g. Flyway).
- Introduce container image scanning (e.g. Trivy).
- Add more advanced static analysis (clang-tidy, clang-format checks).
- Add Ingress and TLS termination for external HTTPS access in Kubernetes.
- Split the project into separate services (backend + frontend) and extend the pipeline accordingly.
For the purposes of the Modern Practices in DevOps course, the current implementation already demonstrates:
- End-to-end CI/CD,
- Docker-based delivery,
- Deployment to Kubernetes,
- Basic security and quality practices.