ErgoHub Team academic project 2025
From Application Code to a Kubernetes Cluster
ErgoHub, a freelance marketplace: a Spring Boot and React application, and the Docker, Jenkins and Ansible path that deployed it to Azure Kubernetes Service
- Period
- February–July 2025
- Team
- Team of two, with Anastasia Zerva
- Context
- Harokopio University of Athens: Distributed Systems, then DevOps Concepts & Tools
Overview
Building the application was only half the problem. The other half was the path from a commit to a running cluster, and making that path repeatable.
ErgoHub is a freelance marketplace with three roles. Clients post projects, and an administrator approves or denies each one before freelancers can see it. Freelancers apply, with a CV if they choose, and the client accepts one application, which starts the work.
It began as the project for the Distributed Systems course at Harokopio University of Athens. In DevOps Concepts & Tools we took it the rest of the way: container images, Jenkins pipelines, Ansible playbooks and a deployment on Azure Kubernetes Service.
We were a team of two, Anastasia Zerva and I, and we both worked across the API, the front end and the delivery. So this page says “we”; the reflection at the end is mine.
It was an academic project, deployed and presented for the course rather than run as a service, so there are no users, traffic or uptime figures to report.
At a glance
- User roles
- Client, freelancer, administrator
- Application
- Java 21, Spring Boot 3, Spring Security, JWT, React, Redux Toolkit, JUnit, Jest
- Data
- PostgreSQL, Spring Data JPA, MinIO
- Delivery
- Docker, Jenkins, Ansible, Azure Container Registry, AKS
- Edge
- NGINX Ingress, cert-manager, Let’s Encrypt TLS
Application
A request, from the browser to the data
User
Client, freelancer or administrator, in a browser
Front end
React and Redux Toolkit, served by NGINX
API
Spring Boot 3 on Java 21, with JWT and role checks
Data
PostgreSQL for records, MinIO for CV files
Delivery
A change, from the repository to the cluster
Source
One Git repository for the API and the front end
CI
Jenkins runs the tests and builds an image
Registry
Azure Container Registry, tagged with the build number
Kubernetes
AKS, rolled out with kubectl or Ansible
Ingress
NGINX Ingress, HTTPS with Let’s Encrypt
The two paths meet in the cluster: the front end and the API that answer each request are the images the delivery path built and rolled out.
Three roles, one workflow
The API is a Spring Boot 3 application on Java 21. The front end is a React application with Redux Toolkit and a dashboard for each role, and it talks to the API over REST.
PENDING
A client posts a project. It waits for an administrator.
APPROVED or DENIED
An administrator approves it or denies it. Freelancers see only approved projects.
IN_PROGRESS
Freelancers apply, with a CV if they choose. The client accepts one application; the others are rejected.
COMPLETED
The freelancer or an administrator marks the work complete.
Access by role
Signing in returns a JSON Web Token, signed with a shared HMAC key and valid for 24 hours, which carries the user’s role and whether the account has been verified. The API keeps no session: Spring Security checks the token on each request and applies role rules per route. Passwords are stored as BCrypt hashes.
Records and files
Users, projects and applications live in PostgreSQL, through Spring Data JPA. A CV sent with an application goes to a MinIO bucket; the application keeps only its path, and the API serves the file back through a download endpoint.
Mail
Administrators verify freelancer accounts, and the API emails the freelancer when that happens. In the local Docker Compose setup, that mail went to MailHog, a test mail server.
Tests
The API has JUnit tests for its services, run against an in-memory H2 database. The front end has Jest tests with Testing Library. Both run in the pipelines before an image is built.
From source to container
The API and the front end each have their own image, built in two stages: the first holds the build tools, the second only what runs.
API
The first stage installs Maven on a Java 21 image and packages the application. The second copies only the jar onto a fresh Java 21 slim image, listening on port 8080. The image build skips the tests, because the pipeline has already run them.
Front end
Node 21 builds the React application; NGINX on Alpine then serves the built files, with a configuration of its own.
Docker Compose ran the whole system on one machine: PostgreSQL 15, the API, the front end, MinIO, MailHog and Jenkins itself.
Both AKS paths push each image with two tags: the build number, which ties the image to the pipeline run that made it, and latest, which the Kubernetes manifests refer to.
Six pipeline definitions, three ways to deploy
The repository has six Jenkinsfiles: one for the API and one for the front end, for each of three ways to deploy.
Three ways to deploy
AKS with kubectl
Jenkinsfile-backendJenkinsfile-frontend
- Registry
- Azure Container Registry
- Deployment
kubectl set image, then a wait for the rollout and a check on the pods
AKS with Ansible
Jenkinsfile-backend-ansibleJenkinsfile-frontend-ansible
- Registry
- Azure Container Registry
- Deployment
- An Ansible playbook applies the whole system in order, then checks it
Minikube with Ansible
Jenkinsfile-backend-minikubeJenkinsfile-frontend-minikube
- Registry
- Docker Hub
- Deployment
- An Ansible playbook deploys to a local Minikube cluster
API
Build and test
- Compile
- Unit Tests
- Integration Tests
- Package
Publish
- Azure Login & Setup
- Build & Push Docker Image
Deploy
- Deploy to Azure AKS
- Health Check
Front end
Build and test
- Checkout
- Install Dependencies
- Lint
- Unit Tests
- Integration Tests
- Build
- Security Audit
Publish
- Azure Login & Setup
- Build & Push Docker Image
Deploy
- Deploy to Azure AKS
- Health Check
- Verify Application
Both kubectl pipelines sign in to Azure with a service principal and to the registry with a token, push the image, point the Deployment at it with kubectl set image, and wait up to five minutes for the rollout before listing the pods. The front-end pipeline also runs ESLint and npm audit, which report problems without failing the build.
The Ansible pipelines hand the deployment to a playbook, which applies the system in order: the namespace, configuration and secrets; PostgreSQL, MailHog and MinIO, then a wait until they are ready; the API and the front end with the new image, and their Services; finally the Ingress, and checks on what is running.
What ran in the cluster
Everything ran in one namespace, freelance, from plain manifests applied with kubectl, whether by a setup script, a pipeline or Ansible. There is no Helm chart.
Ingress
- NGINX ingress controller
- TLS from Let’s Encrypt, through cert-manager
- HTTP redirected to HTTPS
Routes
- /frontend-service:80
- /apibackend-service:8080
Separate host names for the API, the MinIO console and MailHog
Deployments, one replica each
frontend
NGINX serving the React build
- Service
- frontend-service:80
- Storage
- None
- Probes
- liveness, readiness
backend
The Spring Boot API
- Service
- backend-service:8080
- Storage
- None
- Probes
- startup, liveness, readiness
postgres
PostgreSQL 15
- Service
- postgres-service:5432
- Storage
- 5 Gi volume claim
- Probes
- liveness, readiness
minio
Object storage for CV files
- Service
- minio-service:9000 :9001NodePort
- Storage
- 10 Gi volume claim
- Probes
- None
mailhog
Test mail server
- Service
- mailhog:8025 :1025
- Storage
- None
- Probes
- None
Configuration
A ConfigMap, freelance-config, and a Secret, read by the workloads as environment variables
PostgreSQL and MinIO kept their data on persistent volume claims of 5 Gi and 10 Gi. Each ran as a single-replica Deployment rather than a StatefulSet: enough for a course deployment, not a design for failover.
Every container had CPU and memory requests and limits. The API’s probes called /api/home, a path that needs no sign-in, so that Kubernetes could tell an application that was still starting from one that had failed.
A setup script prepared the cluster once: it installed the NGINX ingress controller and cert-manager, created the Let’s Encrypt issuer and applied the manifests. After that, a run of a kubectl pipeline only had to roll out a new image.
What broke when the pieces met
The last week of June 2025 went less into features than into problems that only appear once the pieces run together. Four of them, from the commit history:
Jenkins could not reach the cluster
Jenkins ran in a container, and the local Minikube cluster was outside it. The address in its kubeconfig went through
host.docker.internal, the Minikube IP, a kubectl proxy and the Docker bridge gateway. The AKS pipelines settled it differently: they fetch a kubeconfig from Azure at the start of each run.Health checks against a secured API
Every path behind Spring Security needs a token, and a Kubernetes probe has none. We pointed the probes at
/api/home, a path that needs no sign-in, and added a startup probe with longer delays, so that a slow start was not taken for a failure.The API ran out of memory
The API container was killed for going over its memory. We raised its memory and gave the JVM an explicit heap limit through
JAVA_TOOL_OPTIONS, so that it stayed inside the container’s limit.Names and tags that had to match
The ConfigMap named services that did not match the Services in the manifests, and the Ansible pipelines had to be told which tag to deploy: the build number of the image just pushed, or
latestfor the part that had not changed.
What I took from it
The project moved where I think an application ends. It does not end when the tests pass; it ends when something outside the program can start it, check it and reach it.
Health is an interface
Kubernetes knows an application only through its probes. The path they call is part of the API like any other, with access rules of its own, and if it is wrong, a working service gets restarted.
Configuration crosses boundaries
A service name in a ConfigMap, an image tag passed to a playbook: what broke were values written in one file and read in another. They need the same care as code, and one place where they are defined.
Limits belong to the program
A memory limit on a container is half a setting. The program inside has its own view of how much memory it may use, and the two have to be chosen together.
Written down, not remembered
By the end, signing in to the registry, rolling out an image and waiting for it were steps in a pipeline or a playbook, not commands someone had to remember. That is what made the second deployment the same as the first.