The Local DevOps Production Platform simulates a production-grade payment processing backend system built using modern DevOps and cloud-native practices.
The platform is designed to:
- Run entirely in a local development environment
- Reflect real-world production architecture patterns
- Demonstrate layered backend design
- Simulate infrastructure automation workflows
The system evolves progressively across development stages:
- H2 in-memory database for local validation
- PostgreSQL for containerized and Kubernetes deployment
- Docker-based containerization
- Multi-container orchestration using Docker Compose
- Kubernetes deployment using Kind
- CI/CD automation using GitHub Actions
- Security scanning using Trivy
- Monitoring and observability using Prometheus and Grafana
The objective of this project is to simulate production-level DevOps workflows, backend system design, lifecycle enforcement, and infrastructure management within a controlled local environment.
The platform demonstrates a full DevOps workflow from application development to containerized deployment and automated CI/CD.
- Handles payment creation and lifecycle management.
- Enforces controlled state transitions.
- Records audit history of all lifecycle changes.
- Exposes REST endpoints for external systems.
- Provides monitoring endpoints via Actuator.
- H2 in-memory database
- Used for fast iteration and local validation
- Enables JPA auto-configuration without external dependencies
- PostgreSQL
- Provides ACID compliance
- Enforces relational integrity
- Supports production-grade persistence requirements
- Containerizes the Spring Boot application.
- Ensures consistent runtime environments.
- Eliminates “works on my machine” issues.
- Orchestrates multi-container setup locally.
- Runs application and PostgreSQL together.
- Manages internal container networking.
- Simulates a production Kubernetes cluster locally.
- Manages deployments, services, scaling, and configuration.
- Enables infrastructure-as-code workflows.
- Automates build and test processes.
- Builds container images.
- Integrates vulnerability scanning.
- Simulates real DevOps pipeline workflows.
- Collects application metrics.
- Visualizes operational data.
- Provides observability for performance and reliability.
-
A merchant system sends a payment request to the API.
-
The API creates a payment with initial status
PENDING. -
The service transitions the payment to
PROCESSING. -
Business logic determines final status:
SUCCESSif validation passesFAILEDotherwise
-
Each transition is recorded in a history table.
-
The final payment status is returned to the caller.
-
Monitoring endpoints expose health and metrics data.
This flow demonstrates lifecycle enforcement, auditability, and clean separation of responsibilities.
The platform leverages modern backend and DevOps technologies to simulate a production-grade system lifecycle.
- Spring Boot – REST API development and application framework
- Spring Data JPA – ORM layer for relational persistence
- H2 Database – In-memory database for local development
- PostgreSQL – Production-grade relational database (Docker & Kubernetes stages)
- Spring Boot Actuator – Health and metrics endpoints
- Lombok – Boilerplate code reduction
- Docker – Application containerization
- Docker Compose – Multi-container orchestration for local environments
- Kubernetes (Kind) – Local cluster simulation for deployment management
- GitHub Actions – Automated build, test, and container workflows
- Trivy – Container vulnerability scanning
- Spring Boot Actuator – Application metrics exposure
- Prometheus – Metrics collection
- Grafana – Metrics visualization
- JUnit – Unit testing framework
- Mockito – Mocking framework for isolation testing
The project is structured as a single monorepo containing application code, infrastructure configuration, and operational tooling.
Local-DevOps-Production-Platform/
│
├── app/ # Spring Boot application source code
│
├── docker/ # Docker and Docker Compose configuration
│
├── k8s/ # Kubernetes manifests (Kind deployment)
│
├── monitoring/ # Prometheus and Grafana configuration
│
├── .github/
│ └── workflows/ # CI/CD pipelines (GitHub Actions)
│
├── docs/ # Architecture diagrams and supporting documentation
│
└── README.md # Project documentation
The repository structure reflects separation of concerns:
- Application logic is isolated under
app/. - Container and runtime configuration are separated from source code.
- Kubernetes manifests are maintained independently of application logic.
- CI/CD automation is version-controlled alongside the application.
- Monitoring configuration is modular and extensible.
Before running this project, ensure the following tools are installed:
- Java 17 or later
- Maven 3.9+
- Docker
- Docker Compose
- Git
- kubectl (Kubernetes CLI)
- Kind (Kubernetes in Docker)
- Ubuntu 22.04+ (or compatible Linux distribution)
- Minimum 8GB RAM for Kubernetes and monitoring stack
This section describes the domain model, relational structure, lifecycle behavior, API surface, and architectural decisions that define the Payment Processing Simulation.
The design reflects production-grade backend principles including separation of concerns, auditability, lifecycle enforcement, and relational integrity.
The core domain entity of the platform is Payment.
A payment represents a financial transaction request initiated by a merchant system and processed by the payment service.
| Field | Type | Description |
|---|---|---|
| id | UUID | Unique identifier for the payment |
| amount | BigDecimal | Monetary value of the transaction |
| currency | String | ISO currency code (e.g., USD, EUR) |
| reference | String | External merchant reference |
| customerId | String | Identifier of the customer initiating the payment |
| status | PaymentStatus (Enum) | Current lifecycle state |
| createdAt | Timestamp | Creation timestamp |
| updatedAt | Timestamp | Last modification timestamp |
The payment lifecycle is controlled using a strongly typed enumeration:
- PENDING
- PROCESSING
- SUCCESS
- FAILED
Using an enum ensures lifecycle states remain constrained and predictable.
To preserve auditability and traceability, every status transition is recorded in a separate entity.
| Field | Type | Description |
|---|---|---|
| id | UUID | Unique identifier for history record |
| payment | Payment | Associated payment (Many-to-One) |
| oldStatus | PaymentStatus | Previous lifecycle state |
| newStatus | PaymentStatus | Updated lifecycle state |
| changedAt | Timestamp | Transition timestamp |
This structure enables a full audit trail of lifecycle transitions.
The relational schema enforces normalization and referential integrity.
- id (UUID, Primary Key)
- amount (DECIMAL)
- currency (VARCHAR)
- reference (VARCHAR, UNIQUE)
- customer_id (VARCHAR)
- status (VARCHAR)
- created_at (TIMESTAMP)
- updated_at (TIMESTAMP)
- id (UUID, Primary Key)
- payment_id (UUID, Foreign Key referencing payments.id)
- old_status (VARCHAR)
- new_status (VARCHAR)
- changed_at (TIMESTAMP)
- One Payment can have many PaymentStatusHistory records.
- Foreign key constraints ensure referential integrity.
- Status transitions are stored as immutable records.
- Payment records themselves are not deleted.
The payment lifecycle represents controlled state transitions from creation to final resolution.
-
Merchant sends a payment request.
-
Payment is created with status
PENDING. -
Payment transitions to
PROCESSING. -
Business rule simulation determines outcome:
- If amount > 0 →
SUCCESS - Otherwise →
FAILED
- If amount > 0 →
-
Each transition is recorded in
payment_status_history.
PENDING→PROCESSINGPROCESSING→SUCCESSPROCESSING→FAILED
Invalid transitions are not permitted and are enforced in the Service layer.
The payment service exposes RESTful endpoints for interaction with external systems.
The API follows standard JSON-based request and response patterns typical of production-grade backend services.
POST /payments
Creates a new payment and initiates lifecycle processing.
POST /payments
Content-Type: application/json{
"amount": 100,
"currency": "USD",
"reference": "ORDER-12345",
"customerId": "CUST-001"
}| Field | Type | Description |
|---|---|---|
| amount | BigDecimal | Transaction monetary value |
| currency | String | ISO currency code |
| reference | String | Merchant-provided reference |
| customerId | String | Identifier of the initiating customer |
The request payload is mapped to a dedicated DTO (
CreatePaymentRequest) to decouple the API contract from the persistence entity.
200 OK
{
"id": "UUID",
"amount": 100,
"currency": "USD",
"reference": "ORDER-12345",
"customerId": "CUST-001",
"status": "SUCCESS",
"createdAt": "2026-03-02T19:40:11.712588674",
"updatedAt": "2026-03-02T19:40:11.805804354"
}Upon receiving a valid request:
-
A payment is created with status
PENDING. -
The payment transitions to
PROCESSING. -
Based on business rules, it transitions to either:
SUCCESSFAILED
-
Each transition is recorded in
payment_status_history.
GET /actuator/health
Confirms application availability and monitoring readiness.
Several architectural decisions were made to simulate production-grade system behavior.
Ensures global uniqueness and avoids predictable sequential identifiers.
Maintains:
- Auditability
- Traceability
- Normalized relational design
- Clear lifecycle transparency
Business rules and state transitions are enforced within the Service layer to maintain separation of concerns and prevent invalid status changes.
Payments are not deleted. Lifecycle changes are recorded via state transitions rather than record mutation or removal.
- H2 is used for local development validation.
- PostgreSQL is the intended production database in containerized deployment.
This ensures development flexibility while preserving production realism.
This section describes how the application is built, executed, and validated locally before introducing containerized infrastructure.
The purpose of this stage is to validate:
- Application compilation
- Dependency resolution
- JPA auto-configuration
- Entity mapping
- Repository initialization
- Service-layer lifecycle enforcement
- Database persistence
- Health monitoring endpoints
This ensures the application is functionally stable before Dockerization.
The Spring Boot application was generated using Spring Initializr and placed inside the app/ directory.
- Group:
com.localdevops - Artifact:
payment-service - Packaging:
jar - Java Version: 17
- Spring Boot Starter Web
- Spring Boot Starter Data JPA
- H2 Database (runtime)
- Spring Boot Actuator
- Lombok
- Spring Boot Starter Test
H2 is used for local development validation. PostgreSQL will be introduced in the Docker Compose stage to simulate a production-grade database environment.
To enable local persistence without requiring external infrastructure, an in-memory H2 database is configured.
spring.application.name=payment-service
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=update
spring.h2.console.enabled=true- Enables automatic JPA configuration
- Allows repository beans to initialize
- Automatically generates schema from entities
- Removes dependency on external database setup
- Speeds up local development cycles
This follows incremental development principles.
Navigate to the application directory:
cd app/payment-serviceBuild the project:
./mvnw clean installThis step:
- Compiles source code
- Validates entity mappings
- Ensures dependency resolution
- Produces an executable JAR
Expected output:
BUILD SUCCESS
Start the application:
./mvnw spring-boot:runSuccessful startup logs should include:
Tomcat started on port 8080
Started PaymentServiceApplication
This confirms:
- Embedded Tomcat initialized
- Application context loaded successfully
- JPA repositories registered
- H2 datasource configured properly
Spring Boot Actuator is enabled for runtime monitoring.
Verify application health:
http://localhost:8080/actuator/health
Expected response:
{
"status": "UP"
}This confirms:
- The application is responsive
- Monitoring endpoints are active
- Application context is fully initialized
To validate the service-layer business logic and lifecycle enforcement, a test payment is created using a JSON request body.
curl -X POST http://localhost:8080/payments \
-H "Content-Type: application/json" \
-d '{"amount":100,"currency":"USD","reference":"ORDER-LOCAL-1","customerId":"CUST-LOCAL-1"}'{
"id": "UUID",
"amount": 100,
"currency": "USD",
"reference": "ORDER-LOCAL-1",
"customerId": "CUST-LOCAL-1",
"status": "SUCCESS",
"createdAt": "...",
"updatedAt": "..."
}When the request is processed:
-
A Payment entity is created with status
PENDING -
The service transitions the payment to
PROCESSING -
Based on validation rules:
- If amount > 0 → status becomes
SUCCESS - Otherwise → status becomes
FAILED
- If amount > 0 → status becomes
-
Each transition is recorded in
payment_status_history
This confirms correct service-layer lifecycle enforcement.
Access the H2 console:
http://localhost:8080/h2-console
Connection details:
- JDBC URL:
jdbc:h2:mem:testdb - Username:
sa - Password: (empty)
SELECT * FROM PAYMENTS;SELECT old_status, new_status, changed_at
FROM PAYMENT_STATUS_HISTORY;Expected result:
-
One record in
PAYMENTS -
Two records in
PAYMENT_STATUS_HISTORY- PENDING → PROCESSING
- PROCESSING → SUCCESS
This confirms:
- One-to-many relationship integrity
- UUID primary key generation
- Proper foreign key mapping
- Service transition logic execution
- Audit trail preservation
At the conclusion of local validation, the system supports:
- REST API interaction via JSON request body
- Layered architecture (Controller → Service → Repository)
- Automatic schema generation via JPA
- In-memory database persistence
- Lifecycle transition enforcement
- Audit history tracking
- Runtime health monitoring
This completes functional validation prior to containerization.
After validating the application locally using an in-memory H2 database, the next step is to package the service into containers and introduce a production-style database environment.
This stage introduces:
- Application containerization
- PostgreSQL containerized database
- Docker networking
- Persistent database storage
- Environment-based configuration
- Multi-container orchestration using Docker Compose
This simulates how modern backend systems are deployed in production environments.
The Spring Boot application is containerized using Docker to ensure consistent execution across different environments.
A multi-stage build is used to optimize the final image size.
app/payment-service/Dockerfile
FROM maven:3.9.9-eclipse-temurin-17 AS builder
WORKDIR /build
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests
FROM eclipse-temurin:17-jdk
WORKDIR /app
COPY --from=builder /build/target/payment-service-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","app.jar"]Multi-stage builds separate the build environment from the runtime environment.
Benefits include:
- Smaller final image size
- Improved security
- Reduced attack surface
- Faster container startup
Docker Compose is used to orchestrate multiple containers required for the platform.
The system consists of two services:
- payment-service — Spring Boot application
- postgres-db — PostgreSQL database
docker-compose.yml
version: '3.9'
services:
postgres-db:
image: postgres:15
container_name: postgres-db
environment:
POSTGRES_DB: paymentdb
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
payment-service:
build:
context: ./app/payment-service
container_name: payment-service
ports:
- "8080:8080"
depends_on:
postgres-db:
condition: service_healthy
environment:
DB_URL: jdbc:postgresql://postgres-db:5432/paymentdb
DB_USERNAME: postgres
DB_PASSWORD: postgres
volumes:
postgres_data:Docker Compose automatically creates an isolated network for all services.
This allows containers to communicate using service names as hostnames.
Example:
jdbc:postgresql://postgres-db:5432/paymentdb
Here:
postgres-dbis the service name- Docker resolves it to the correct container IP address
This removes the need for manual network configuration.
PostgreSQL stores database files inside the container path:
/var/lib/postgresql/data
A Docker named volume is mounted to preserve this data.
volumes:
postgres_data:
This ensures:
- Data survives container restarts
- Database state is preserved
- Application data is not lost
Without volumes, containers would lose all data when restarted.
The application uses environment variables for database configuration.
Inside application.properties:
spring.datasource.url=${DB_URL}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}Docker Compose injects these variables at runtime.
This approach follows 12-Factor App principles and enables:
- Environment portability
- Secure configuration management
- Separation of config from code
From the project root directory:
docker compose up --buildThis command will:
- Build the Spring Boot container image
- Start the PostgreSQL container
- Wait for database readiness
- Start the application container
- Establish container networking
Successful startup logs will show:
Tomcat started on port 8080
Started PaymentServiceApplication
Once containers are running, the API can be tested from the host machine.
curl -X POST http://localhost:8080/payments \
-H "Content-Type: application/json" \
-d '{"amount":200,"currency":"USD","reference":"DOCKER-1","customerId":"CUST-DOCKER"}'Response:
{
"id": "6bf524cc-6d9f-4153-8bc8-d1aea3b2fa20",
"amount": 200,
"currency": "USD",
"reference": "DOCKER-1",
"customerId": "CUST-DOCKER",
"status": "SUCCESS",
"createdAt": "...",
"updatedAt": "..."
}To verify that data is stored inside PostgreSQL:
Enter the database container:
docker exec -it postgres-db psql -U postgres -d paymentdbQuery the payments table:
SELECT * FROM payments;Query the status history:
SELECT * FROM payment_status_history;This confirms:
- PostgreSQL persistence works
- Application successfully writes to the containerized database
- Payment lifecycle history is correctly stored
At the end of this stage, the system supports:
- Containerized Spring Boot application
- Containerized PostgreSQL database
- Docker Compose orchestration
- Service networking
- Health-based startup ordering
- Persistent database storage
- Environment-based configuration
This establishes a production-style foundation before introducing Kubernetes deployment.
After validating the application using Docker Compose, the next step is deploying the system to a Kubernetes environment.
For local Kubernetes orchestration, Kind (Kubernetes in Docker) is used. Kind runs a fully functional Kubernetes cluster inside Docker containers, making it suitable for development and testing.
This stage introduces:
- Kubernetes cluster creation
- Container image loading into Kind
- Kubernetes manifests for application deployment
- Service exposure inside the cluster
- Pod orchestration and lifecycle management
This simulates how applications are deployed and managed in real production Kubernetes environments.
A local Kubernetes cluster is created using Kind.
Command:
kind create cluster --name devops-platformThis command initializes a Kubernetes control-plane node running inside Docker.
Expected output:
Then Verify the Cluster
Run:
kubectl get nodesBefore deploying the application to Kubernetes, the container image must be built locally.
Navigate to the project root directory and build the image using Docker.
docker build -t payment-service:1.0 ./app/payment-serviceThis command performs the following:
- Builds the application container image using the Dockerfile
- Tags the image as
payment-service:1.0 - Stores the image in the local Docker image registry
Verify the image was created:
docker images | grep payment-serviceExpected output:
This confirms the container image is ready to be deployed.
Kind clusters run inside Docker containers. Because of this, Kubernetes nodes cannot directly access images stored on the host machine.
To make the application image available inside the Kind cluster, it must be manually loaded.
Command:
kind load docker-image payment-service:1.0 --name devops-platformThis command transfers the locally built Docker image into the Kind cluster node.
Once loaded, Kubernetes can use the image when creating Pods and Deployments.
Kubernetes resources are defined using YAML manifests.
For this project, all Kubernetes configuration files are stored inside a dedicated k8s directory.
From your project root:
mkdir k8sYour project structure will now look like:
Local-DevOps-Production-Platform
│
├── app/
├── docker-compose.yml
├──k8s/
app-deployment.yaml
app-service.yaml
└── README.md
These manifests define how the application is deployed and exposed inside the Kubernetes cluster.
Now We Create the First Manifest
Inside the new folder:
cd k8sCreate the deployment file:
nano app-deployment.yamlPaste this.
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
spec:
replicas: 1
selector:
matchLabels:
app: payment-service
template:
metadata:
labels:
app: payment-service
spec:
containers:
- name: payment-service
image: payment-service:1.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: docker
- name: DB_URL
value: jdbc:postgresql://postgres:5432/paymentdb
- name: DB_USERNAME
value: postgres
- name: DB_PASSWORD
value: postgres
What This Deployment Does
This manifest tells Kubernetes:
- run 1 replica
- start container payment-service
- use image payment-service:1.0
- expose port 8080
Kubernetes will create a Pod running your container.
To run the application inside the Kubernetes cluster, a Deployment resource is created.
A Deployment ensures that the desired number of application Pods are running and automatically replaces failed Pods when necessary. It also enables scaling and rolling updates in production environments.
The deployment configuration is defined in the file:
k8s/app-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
spec:
replicas: 1
selector:
matchLabels:
app: payment-service
template:
metadata:
labels:
app: payment-service
spec:
containers:
- name: payment-service
image: payment-service:1.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: docker
- name: DB_URL
value: jdbc:postgresql://postgres:5432/paymentdb
- name: DB_USERNAME
value: postgres
- name: DB_PASSWORD
value: postgres
| Field | Description |
|---|---|
apiVersion |
Specifies the Kubernetes API version used for Deployment resources |
kind |
Defines the resource type as a Deployment |
metadata.name |
Unique name for the Deployment inside the cluster |
replicas |
Number of application instances Kubernetes should maintain |
selector.matchLabels |
Identifies which Pods belong to this Deployment |
template.metadata.labels |
Labels assigned to Pods created by the Deployment |
containers.name |
Logical name of the container |
containers.image |
Container image loaded into the Kind cluster |
containerPort |
Port exposed by the application container |
The container image payment-service:1.0 was previously loaded into the Kind cluster using the command:
kind load docker-image payment-service:1.0 --name devops-platformThis allows Kubernetes to start the application without pulling the image from an external container registry.
To create the Deployment inside the Kubernetes cluster, run:
kubectl apply -f k8s/app-deployment.yamlSuccessful output:
deployment.apps/payment-service created
Check the running Pods:
kubectl get podsThis confirms that Kubernetes successfully created a Pod running the payment-service container.
To monitor the deployment status:
kubectl get deploymentsExpected output:
This indicates that the application deployment is healthy and running inside the Kubernetes cluster.
The payment service requires a PostgreSQL database for persistent storage.
To provide this dependency inside the Kubernetes cluster, PostgreSQL is deployed as a separate Deployment resource.
Deployment file location:
k8s/postgres-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15
ports:
- containerPort: 5432
env:
- name: POSTGRES_DB
value: paymentdb
- name: POSTGRES_USER
value: postgres
- name: POSTGRES_PASSWORD
value: postgres| Field | Description |
|---|---|
Deployment |
Ensures PostgreSQL pod is always running |
replicas |
Specifies one database instance |
image |
Official PostgreSQL container image |
containerPort |
Database port exposed inside the cluster |
POSTGRES_DB |
Database name automatically created at startup |
POSTGRES_USER |
Database username |
POSTGRES_PASSWORD |
Database password |
Deploy the PostgreSQL database to the Kubernetes cluster:
kubectl get deploymentsVerify the deployment:
kubectl get podsThis confirms that the PostgreSQL database is running inside the Kubernetes cluster.
To allow other pods to communicate with PostgreSQL, create a Kubernetes Service.
Service file location:
k8s/postgres-service.yamlapiVersion: v1
kind: Service
metadata:
name: postgres
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432Kubernetes services provide stable networking endpoints for pods.
The service name postgres becomes an internal DNS entry inside the cluster.
Applications connect to the database using:
postgres:5432Database connection string:
jdbc:postgresql://postgres:5432/paymentdbDeploy the service:
kubectl apply -f k8s/postgres-service.yamlKubernetes Cluster
│
├── postgres (Deployment + Service)
│
└── payment-service (Deployment)
Pods communicate internally using:
postgres:5432
Check that both pods are running:
kubectl get podsExpected output:
Kubernetes Pods are not directly accessible from outside the cluster.
To expose the application, a Service resource is created.
The service acts as a stable network endpoint that forwards external traffic to the application Pods.
Service file location:
k8s/app-service.yaml
apiVersion: v1
kind: Service
metadata:
name: payment-service
spec:
type: NodePort
selector:
app: payment-service
ports:
- port: 8080
targetPort: 8080
nodePort: 30007| Field | Description |
|---|---|
type: NodePort |
Exposes the service outside the cluster |
selector |
Routes traffic to Pods labeled payment-service |
port |
Service port inside the cluster |
targetPort |
Container port inside the Pod |
nodePort |
External port used to access the application |
Create the service with:
kubectl apply -f k8s/app-service.yamlExpected output:
service/payment-service created
Run:
kubectl get servicesOutput:
For Kind-based Kubernetes clusters, NodePort services are not directly exposed on the host machine because the Kubernetes node itself runs inside a Docker container.
To access the application from the local machine, port forwarding is used. This forwards traffic from the host to the Kubernetes Service inside the cluster.
Run the following command:
kubectl port-forward service/payment-service 8080:8080Expected output:
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080
Once the port forwarding session is active, the application becomes accessible locally at:
http://localhost:8080
After exposing the service, the API can be tested to verify that the Kubernetes deployment is functioning correctly.
Run the following request:
curl -X POST http://localhost:8080/payments \
-H "Content-Type: application/json" \
-d '{"amount":500,"currency":"USD","reference":"K8S-TEST-2","customerId":"CUST-K8S"}'Response:
{
"id": "UUID",
"amount": 500,
"currency": "USD",
"reference": "K8S-TEST-2",
"customerId": "CUST-K8S",
"status": "SUCCESS"
}This confirms that:
- The Kubernetes Service is correctly routing traffic to the application Pod
- The Spring Boot API is operational inside the cluster
- The application successfully communicates with the PostgreSQL database
- The full request lifecycle executes correctly within the Kubernetes environment
This completes the validation of the application deployment inside the Kind-based Kubernetes cluster.
The application is deployed inside a Kubernetes cluster created using Kind (Kubernetes in Docker). The architecture consists of multiple Kubernetes resources working together to run and expose the payment service.
Client
│
│ HTTP Request
▼
kubectl port-forward
│
▼
Kubernetes Service (payment-service)
│
▼
Payment-Service Pod
(Spring Boot Application)
│
│ JDBC Connection
▼
Kubernetes Service (postgres)
│
▼
PostgreSQL Pod
(Database)
| Component | Description |
|---|---|
| Kind Cluster | Local Kubernetes cluster running inside Docker |
| Deployment (payment-service) | Manages the lifecycle of the Spring Boot application Pod |
| Service (payment-service) | Exposes the application inside the cluster and allows port-forward access |
| Deployment (postgres) | Runs the PostgreSQL database container |
| Service (postgres) | Provides a stable DNS endpoint (postgres:5432) for database connectivity |
| Pod Networking | Enables internal communication between application and database Pods |
Kubernetes services provide built-in DNS resolution, allowing the application to connect to PostgreSQL using the service name:
jdbc:postgresql://postgres:5432/paymentdb
This eliminates the need for fixed IP addresses and enables dynamic service discovery within the cluster.
At the end of this stage, the platform successfully demonstrates:
- Containerized application deployment
- Kubernetes Pod orchestration
- Internal service networking
- Stateful database connectivity
- Local cluster exposure using port-forwarding
This completes the Kubernetes deployment stage of the project and prepares the platform for CI/CD automation and production-grade DevOps workflows. Sir, your section is already strong. I’ve refined it to sound more professional, clearer, and more consistent with high-quality GitHub README standards while keeping everything easy to paste directly into your file.
To automate the build and containerization process, a Continuous Integration and Continuous Deployment (CI/CD) pipeline is implemented using GitHub Actions.
The pipeline is automatically triggered whenever code is pushed to the main branch. This ensures that every change to the codebase is validated, built, and packaged consistently without manual intervention.
The pipeline performs the following stages:
- Source code checkout
- Java environment setup
- Application build using Maven
- Docker image build
- Docker image push to Docker Hub
The workflow configuration file is located at:
.github/workflows/ci-cd-pipeline.yml
name: CI/CD Pipeline
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- name: Build application
run: |
cd app/payment-service
mvn clean package -DskipTests
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Build Docker image
run: |
docker build -t ${{ secrets.DOCKER_HUB_USERNAME }}/payment-service:${{ github.sha }} ./app/payment-service
- name: Push Docker image
run: |
docker push ${{ secrets.DOCKER_HUB_USERNAME }}/payment-service:${{ github.sha }}The following screenshot shows a successful CI/CD pipeline run in GitHub Actions.
When code is pushed to the main branch, the pipeline automatically executes the following steps.
The pipeline retrieves the latest version of the source code from the GitHub repository using the actions/checkout action.
Java 17 is installed using actions/setup-java to ensure compatibility with the Spring Boot application.
The application is compiled and packaged using Maven:
mvn clean package -DskipTestsThis process generates the executable Spring Boot JAR file required to run the application.
A Docker image containing the packaged application is built using the project's Dockerfile.
After the image is successfully built, it is pushed to Docker Hub, making it available for deployment in containerized environments.
This process ensures that the latest version of the application is always stored in the container registry and ready for deployment.
To improve traceability and support reliable deployments, Docker images can be tagged using the Git commit SHA generated during the pipeline execution.
Example image tag:
payment-service:4bfa1c3
- Each build produces a uniquely identifiable image
- Enables easy rollback to previous versions
- Ensures reproducible deployments
- Improves traceability between source code commits and container images
To enhance security within the CI/CD pipeline, container vulnerability scanning is integrated using Trivy.
Trivy scans the Docker image for known vulnerabilities before the image is pushed to the container registry.
The scan checks for:
- Operating system vulnerabilities
- Application dependency vulnerabilities
- Known CVEs in container images
During the CI/CD pipeline execution, Trivy analyzes the built Docker image:
docker image → vulnerability scan → push to registry
The scan focuses on high-risk vulnerabilities:
- CRITICAL
- HIGH
This ensures potential security risks are detected early in the development lifecycle.
Open your workflow file:
nano .github/workflows/ci-cd-pipeline.ymlAdd the Trivy scan step after the Docker image build.
name: CI/CD Pipeline
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Build application
run: |
cd app/payment-service
mvn clean package -DskipTests
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Build Docker image
run: |
docker build -t ${{ secrets.DOCKER_HUB_USERNAME }}/payment-service:${{ github.sha }} ./app/payment-service
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@0.20.0
with:
image-ref: ${{ secrets.DOCKER_HUB_USERNAME }}/payment-service:${{ github.sha }}
format: table
exit-code: 0
ignore-unfixed: true
vuln-type: 'os,library'
severity: 'CRITICAL,HIGH'
- name: Push Docker image
run: |
docker push ${{ secrets.DOCKER_HUB_USERNAME }}/payment-service:${{ github.sha }}Run:
git add .
git commit -m "Add Trivy container vulnerability scanning to CI pipeline"
git push- Improves container security posture
- Detects vulnerabilities before deployment
- Aligns with DevSecOps practices
- Helps maintain secure software supply chains
To ensure the reliability and health of the application in a Kubernetes environment, monitoring and observability mechanisms are integrated using Spring Boot Actuator and Kubernetes health probes.
These mechanisms allow Kubernetes to automatically detect unhealthy containers and take corrective actions such as restarting pods or temporarily removing them from service routing.
Spring Boot Actuator exposes operational endpoints that provide insights into the application’s health and runtime status.
Key endpoints include:
/actuator/health
/actuator/health/liveness
/actuator/health/readiness
Example health check:
curl http://localhost:8080/actuator/healthResponse:
{
"status": "UP",
"groups": [
"liveness",
"readiness"
]
}These endpoints enable external systems such as Kubernetes to verify whether the application is running correctly.
The liveness probe checks whether the application process is still running.
If the probe fails, Kubernetes automatically restarts the container to recover from failure conditions.
Example scenarios where liveness probes help:
- Application deadlocks
- Infinite loops
- Runtime crashes
Configuration used in the deployment manifest:
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 10The readiness probe determines whether the application is ready to serve incoming traffic.
If the readiness probe fails, Kubernetes removes the pod from the service load balancer until the application becomes healthy again.
Common situations include:
- Application startup in progress
- Database connection unavailable
- Dependency services not ready
Configuration:
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 60
periodSeconds: 5To ensure efficient cluster resource usage, CPU and memory requests and limits are defined for the application container.
Resource configuration:
resources:
requests:
memory: "256Mi"
cpu: "200m"
limits:
memory: "512Mi"
cpu: "500m"Resource requests guarantee that Kubernetes reserves the required resources for the container, while limits prevent the container from consuming excessive resources that could affect other workloads in the cluster.
The monitoring and observability configuration provides several advantages:
- Automatic container recovery
- Improved system stability
- Controlled resource consumption
- Early detection of application failures
- Reliable service availability in Kubernetes
These mechanisms help maintain application resilience in production environments.
While the platform demonstrates a full DevOps workflow—from application development to containerization, Kubernetes orchestration, CI/CD automation, and security scanning—several enhancements could further improve the system for production-scale environments.
Currently, the CI/CD pipeline builds and pushes container images to Docker Hub. Future improvements could include automatic deployment to Kubernetes clusters using tools such as:
- Helm
- ArgoCD
- FluxCD
These tools enable GitOps workflows, where infrastructure changes are automatically applied from version-controlled repositories.
Although the pipeline currently uses commit SHA tagging, future improvements could include advanced image management strategies such as:
- semantic versioning
- automated rollback mechanisms
- deployment promotion between environments (dev, staging, production)
This would enhance traceability and deployment reliability.
Monitoring can be extended by integrating a full observability stack such as:
- Prometheus (metrics collection)
- Grafana (visual dashboards)
- Loki (centralized logging)
This would allow deeper visibility into application performance and system behavior.
The infrastructure components could be provisioned using Infrastructure as Code tools such as:
- Terraform
- AWS CloudFormation
This would allow automated provisioning of cloud resources, ensuring reproducible infrastructure environments.
Future iterations could introduce multiple deployment environments, including:
- Development
- Staging
- Production
Each environment could be managed through separate Kubernetes namespaces or clusters to ensure safer release processes.
Additional DevSecOps practices could be introduced, including:
- container image signing
- secret management using tools such as HashiCorp Vault
- runtime security monitoring
These measures would strengthen the security posture of the platform.
These improvements would transform the platform into a fully production-ready DevOps ecosystem capable of supporting scalable and secure application deployments.
Philip Oluwaseyi Oludolamu
DevOps Engineer | Cloud Infrastructure | CI/CD Automation
📧 Email: oluphilix@gmail.com
📱 Phone: +90 533 876 3067
🔗 LinkedIn: https://www.linkedin.com/in/holuphilix
💻 GitHub: https://github.com/Holuphilix

















