Docker + Docker Compose for Spring Boot app
Kubernetes manifests (Deployment, Service, HorizontalPodAutoscaler, ConfigMap, Secret)
GitHub Actions CI/CD workflow (build, test, build Docker image, push to registry)
Android app updated to MVVM (Kotlin) with Retrofit integration and simple persistence
placeholders is the place where you must insert credentials (Docker registry, secrets). Copy/paste into your projects and adjust names/credentials.
Docker (Spring Boot)
Dockerfile
# Use a multi-stage build for a Spring Boot jar
FROM eclipse-temurin:17-jdk-jammy AS build
WORKDIR /app
COPY pom.xml mvnw ./
COPY .mvn .mvn
COPY src src
RUN ./mvnw -B -DskipTests package
FROM eclipse-temurin:17-jre-jammy
ARG JAR_FILE=target/*.jar
COPY --from=build /app/${JAR_FILE} /app/app.jar
EXPOSE 8080
ENTRYPOINT ["java","-Xms256m","-Xmx512m","-jar","/app/app.jar"]
.dockerignore
target/
.vscode/
.idea/
*.iml
.mvn/wrapper/maven-wrapper.jar
docker-compose.yml (app + mysql)
version: "3.8"
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: examplepassword
MYSQL_DATABASE: profitlossdb
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
profitloss-app:
build: .
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/profitlossdb?useSSL=false&serverTimezone=UTC
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: examplepassword
depends_on:
- mysql
ports:
- "8080:8080"
volumes:
mysql-data:
Kubernetes manifests
Assume namespace profitloss. Secrets contain DB password and Docker registry creds — replace placeholders.
k8s/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: profitloss
k8s/secret.yaml (DB password & optional registry)
apiVersion: v1
kind: Secret
metadata:
name: profitloss-secret
namespace: profitloss
type: Opaque
stringData:
DB_PASSWORD: "examplepassword" # replace
REGISTRY_USERNAME: "your-registry-user" # replace if pushing private image
REGISTRY_PASSWORD: "registry-pass" # replace
k8s/configmap.yaml (app config)
apiVersion: v1
kind: ConfigMap
metadata:
name: profitloss-config
namespace: profitloss
data:
SPRING_DATASOURCE_URL: jdbc:mysql://profitloss-mysql:3306/profitlossdb?useSSL=false&serverTimezone=UTC
SPRING_DATASOURCE_USERNAME: root
SPRING_JPA_HIBERNATE_DDL_AUTO: update
k8s/mysql-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: profitloss-mysql
namespace: profitloss
spec:
replicas: 1
selector:
matchLabels:
app: profitloss-mysql
template:
metadata:
labels:
app: profitloss-mysql
spec:
containers:
- name: mysql
image: mysql:8.0
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: profitloss-secret
key: DB_PASSWORD
- name: MYSQL_DATABASE
value: profitlossdb
ports:
- containerPort: 3306
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
volumes:
- name: mysql-data
persistentVolumeClaim:
claimName: mysql-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pvc
namespace: profitloss
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5Gi
k8s/app-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: profitloss-app
namespace: profitloss
spec:
replicas: 2
selector:
matchLabels:
app: profitloss-app
template:
metadata:
labels:
app: profitloss-app
spec:
containers:
- name: profitloss-app
image: your-registry/profitloss-service:latest # replace with built image
imagePullPolicy: IfNotPresent
env:
- name: SPRING_DATASOURCE_URL
valueFrom:
configMapKeyRef:
name: profitloss-config
key: SPRING_DATASOURCE_URL
- name: SPRING_DATASOURCE_USERNAME
valueFrom:
configMapKeyRef:
name: profitloss-config
key: SPRING_DATASOURCE_USERNAME
- name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef:
name: profitloss-secret
key: DB_PASSWORD
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: profitloss-service
namespace: profitloss
spec:
selector:
app: profitloss-app
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: ClusterIP
k8s/hpa.yaml (autoscale)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: profitloss-hpa
namespace: profitloss
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: profitloss-app
minReplicas: 2
maxReplicas: 6
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
Optional: add Ingress manifest depending on your cluster.
GitHub Actions — CI/CD
This workflow:
- Runs tests
- Builds jar
- Builds Docker image
- Logs into Docker registry (Docker Hub/GitHub Packages)
- Pushes image
- Optionally applies Kubernetes manifests via
kubectl(if you configure KUBE_CONFIG)
Create .github/workflows/ci-cd.yml:
name: CI/CD Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
IMAGE_NAME: your-registry/profitloss-service
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: Build and run tests
run: ./mvnw -B clean verify
build-and-push:
needs: build-test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to registry
uses: docker/login-action@v2
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }} # set in repo secrets
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ env.IMAGE_NAME }}:latest
deploy-k8s:
needs: build-and-push
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup kubectl
uses: azure/setup-kubectl@v3
with:
version: 'latest'
- name: Configure kubectl
run: |
echo "${{ secrets.KUBE_CONFIG }}" > kubeconfig
export KUBECONFIG=$PWD/kubeconfig
- name: Update image in deployment (kubectl set image)
run: |
kubectl -n profitloss set image deployment/profitloss-app profitloss-app=${{ env.IMAGE_NAME }}:latest || true
kubectl -n profitloss rollout status deployment/profitloss-app --timeout=120s || true
Secrets to add in repo settings
DOCKERHUB_USERNAME,DOCKERHUB_TOKEN(or GitHub Packages token)KUBE_CONFIG— base64 encoded kubeconfig or raw kubeconfig contents (use caution)
Android — MVVM architecture (Kotlin)
I’ll provide a minimal, clean MVVM structure with Retrofit + LiveData + ViewModel + Repository.
Gradle dependencies (app/build.gradle)
plugins {
id 'com.android.application'
id 'kotlin-android'
}
android {
compileSdk 34
defaultConfig {
applicationId "com.example.profitlossapp"
minSdk 21
targetSdk 34
versionCode 1
versionName "1.0"
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.0"
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
}
Network layer (Retrofit)
network/ProfitLossApi.kt
package com.example.profitlossapp.network
import com.example.profitlossapp.model.CalculateRequest
import com.example.profitlossapp.model.CalculateResponse
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.POST
interface ProfitLossApi {
@POST("/api/calculate")
suspend fun calculate(@Body req: CalculateRequest): Response<CalculateResponse>
}
network/RetrofitClient.kt
package com.example.profitlossapp.network
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object RetrofitClient {
private const val BASE_URL = "http://10.0.2.2:8080" // emulator -> host
val api: ProfitLossApi by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ProfitLossApi::class.java)
}
}
Models
model/CalculateRequest.kt
package com.example.profitlossapp.model
data class CalculateRequest(val cp: Double, val sp: Double)
model/CalculateResponse.kt
package com.example.profitlossapp.model
data class CalculateResponse(val type: String, val amount: Double, val percent: Double, val id: Long)
Repository
repository/ProfitLossRepository.kt
package com.example.profitlossapp.repository
import com.example.profitlossapp.model.CalculateRequest
import com.example.profitlossapp.model.CalculateResponse
import com.example.profitlossapp.network.RetrofitClient
import retrofit2.Response
class ProfitLossRepository {
suspend fun calculate(req: CalculateRequest): Response<CalculateResponse> {
return RetrofitClient.api.calculate(req)
}
}
ViewModel
ui/MainViewModel.kt
package com.example.profitlossapp.ui
import androidx.lifecycle.*
import com.example.profitlossapp.model.CalculateRequest
import com.example.profitlossapp.model.CalculateResponse
import com.example.profitlossapp.repository.ProfitLossRepository
import kotlinx.coroutines.launch
class MainViewModel : ViewModel() {
private val repo = ProfitLossRepository()
private val _result = MutableLiveData<CalculateResponse?>()
val result: LiveData<CalculateResponse?> = _result
private val _error = MutableLiveData<String?>()
val error: LiveData<String?> = _error
fun calculate(cp: Double, sp: Double) {
viewModelScope.launch {
try {
val response = repo.calculate(CalculateRequest(cp, sp))
if (response.isSuccessful) {
_result.value = response.body()
_error.value = null
} else {
_error.value = "Server error: ${response.code()}"
}
} catch (e: Exception) {
_error.value = e.message
}
}
}
}
UI (Activity)
ui/MainActivity.kt
package com.example.profitlossapp.ui
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.example.profitlossapp.R
class MainActivity : AppCompatActivity() {
private val vm: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val etCP = findViewById<EditText>(R.id.etCP)
val etSP = findViewById<EditText>(R.id.etSP)
val btn = findViewById<Button>(R.id.btnCalculate)
val tv = findViewById<TextView>(R.id.tvResult)
btn.setOnClickListener {
val cp = etCP.text.toString().toDoubleOrNull()
val sp = etSP.text.toString().toDoubleOrNull()
if (cp == null || sp == null) {
tv.text = "Enter valid numbers"
return@setOnClickListener
}
vm.calculate(cp, sp)
}
vm.result.observe(this) { resp ->
resp?.let {
tv.text = "Type: ${it.type}\nAmount: ${it.amount}\nPercent: ${it.percent}\nID: ${it.id}"
}
}
vm.error.observe(this) { err ->
err?.let { tv.text = "Error: $it" }
}
}
}
Layout (res/layout/activity_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:padding="24dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText android:id="@+id/etCP" android:hint="Cost Price" android:inputType="numberDecimal" android:layout_width="match_parent" android:layout_height="wrap_content"/>
<EditText android:id="@+id/etSP" android:hint="Selling Price" android:inputType="numberDecimal" android:layout_marginTop="12dp" android:layout_width="match_parent" android:layout_height="wrap_content"/>
<Button android:id="@+id/btnCalculate" android:text="Calculate" android:layout_marginTop="16dp" android:layout_width="match_parent" android:layout_height="wrap_content"/>
<TextView android:id="@+id/tvResult" android:textSize="16sp" android:layout_marginTop="18dp" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
</LinearLayout>
Quick instructions & notes
- Docker:
docker build -t your-registry/profitloss-service:latest .thendocker push ...(or use the GitHub Actions workflow). - Docker Compose:
docker compose up --build - Kubernetes:
kubectl apply -f k8s/namespace.yaml && kubectl apply -f k8s/secret.yaml && kubectl apply -f k8s/configmap.yaml && kubectl apply -f k8s/mysql-deployment.yaml && kubectl apply -f k8s/app-deployment.yaml && kubectl apply -f k8s/hpa.yaml - GitHub Actions: set repository secrets (
DOCKERHUB_USERNAME,DOCKERHUB_TOKEN,KUBE_CONFIG). - Android MVVM: run emulator and ensure Spring Boot is reachable — use
10.0.2.2for the emulator or your machine IP for a real device. Allow CORS on Spring Boot if calling from other origins (add@CrossOriginto controller or CorsFilter).