Skip to content
More work

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
Two paths through the project

Application

A request, from the browser to the data

  1. User

    Client, freelancer or administrator, in a browser

  2. Front end

    React and Redux Toolkit, served by NGINX

  3. API

    Spring Boot 3 on Java 21, with JWT and role checks

  4. Data

    PostgreSQL for records, MinIO for CV files

Delivery

A change, from the repository to the cluster

  1. Source

    One Git repository for the API and the front end

  2. CI

    Jenkins runs the tests and builds an image

  3. Registry

    Azure Container Registry, tagged with the build number

  4. Kubernetes

    AKS, rolled out with kubectl or Ansible

  5. 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.

A project, from posting to completion
  1. PENDING

    A client posts a project. It waits for an administrator.

  2. APPROVED or DENIED

    An administrator approves it or denies it. Freelancers see only approved projects.

  3. IN_PROGRESS

    Freelancers apply, with a CV if they choose. The client accepts one application; the others are rejected.

  4. 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
Stages of the two kubectl pipelines, as named in their Jenkinsfiles

API

Build and test

  1. Compile
  2. Unit Tests
  3. Integration Tests
  4. Package

Publish

  1. Azure Login & Setup
  2. Build & Push Docker Image

Deploy

  1. Deploy to Azure AKS
  2. Health Check

Front end

Build and test

  1. Checkout
  2. Install Dependencies
  3. Lint
  4. Unit Tests
  5. Integration Tests
  6. Build
  7. Security Audit

Publish

  1. Azure Login & Setup
  2. Build & Push Docker Image

Deploy

  1. Deploy to Azure AKS
  2. Health Check
  3. 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.

The freelance namespace on Azure Kubernetes Service

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 latest for 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.

Back to the projects