Devopsified and Deployment of Java Micronaut Web Application into k3s kubernetes cluster with custom domain , ssl certificates and monitoring.

Saurav KarkiSaurav Karki
4 min read

In this project, I focused on complete DevOps implementation of a three-tier web application built using Micronaut, React, and ScyllaDB. The application was containerized, deployed in a self-hosted lightweight K3s Kubernetes cluster, and exposed securely over a custom domain with Let's Encrypt SSL certificates and Traefik Ingress.

i) Containerization and Image Management

The frontend (React) and backend (Micronaut) components were:

  • Dockerized separately with production-ready Dockerfiles.

  • Pushed to Docker Hub for use in Kubernetes deployment.
    This enabled version-controlled, reproducible deployments across environments.

ii) Kubernetes Deployment on K3s

A K3s cluster was manually provisioned and configured to run the application:

  • Kubernetes manifests were written for deployments, services, and ingress rules.

  • Each tier (frontend, backend, database) was independently deployed in its own pod.

  • The backend services connected to a ScyllaDB cluster set up on two virtual machines.

iii) Ingress & Domain Setup with Traefik and SSL

Traefik was used as the Kubernetes ingress controller to handle traffic routing:

  • Ingress resources were defined with custom routing rules for / and /api paths.

  • The domain notes.ksaurav.com.np was mapped to the Traefik LoadBalancer External IP.

  • Use Cloud Flare as a DNS Manager.

  • Let’s Encrypt certificates were issued automatically using cert-manager and a configured ClusterIssuer.

Figure : Kubernetes manifest structure of the project

Figure : Details of the nodes and running resources in k8s cluster

Figure : Final Deployed Three Tier notes maker app with SSL certificates and Custom Domain.

Figure : Grafana Dashboard for application metrics monitoring

Figure : Lens Dashboard for Cluster Management

Figure : New Relic Dashboard for K8s cluster and resource monitoring

Complete manifests of the project:

Backend-deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: notes-app-backend
  namespace: notes-maker
spec:
  replicas: 1
  selector:
    matchLabels:
      app: notes-app-backend
  template:
    metadata:
      labels:
        app: notes-app-backend
    spec:
      containers:
      - name: notes-app-backend
        image: sauravkarki/backend:latest
        ports:
        - containerPort: 8080
        env:
        - name: MICRONAUT_SERVER_HOST
          value: "0.0.0.0"
        - name: MICRONAUT_SERVER_PORT
          value: "8080"
        - name: MICRONAUT_SERVER_CORS_ENABLED
          value: "true"
        - name: MICRONAUT_SERVER_CORS_CONFIGURATIONS_WEB_ALLOWED_ORIGINS
          value: "https://notes.ksaurav.com.np,https://notes.ksaurav.com.np,https://45.79.121.155:30000,https://localhost:3000,https://172.105.36.6:30000"
        - name: MICRONAUT_SERVER_CORS_CONFIGURATIONS_WEB_ALLOWED_METHODS
          value: "GET,POST,PUT,DELETE,OPTIONS"
        - name: MICRONAUT_SERVER_CORS_CONFIGURATIONS_WEB_ALLOWED_HEADERS
          value: "Content-Type,Authorization,X-Requested-With"
        - name: CASSANDRA_DEFAULT_CONTACT_POINTS
          value: "192.168.148.77,192.168.130.98"
        - name: CASSANDRA_DEFAULT_PORT
          value: "9042"
        - name: CASSANDRA_DEFAULT_DATACENTER
          value: "DC1"
        - name: CASSANDRA_DEFAULT_KEYSPACE
          value: "notes_maker"
        - name: LOG_LEVEL
          value: "DEBUG"
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 20
          periodSeconds: 5
        resources:
          requests:
            memory: "512Mi"
            cpu: "250m"
          limits:
            memory: "1Gi"
            cpu: "500m"

backend-service:

apiVersion: v1
kind: Service
metadata:
  name: notes-backend-service
  namespace: notes-maker
spec:
  selector:
    app: notes-app-backend
  ports:
  - port: 8080
    targetPort: 8080
    protocol: TCP
  type: ClusterIP

frontend-deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: notes-frontend
  namespace: notes-maker
spec:
  replicas: 2
  selector:
    matchLabels:
      app: notes-frontend
  template:
    metadata:
      labels:
        app: notes-frontend
    spec:
      containers:
      - name: notes-frontend
        image: sauravkarki/frontend:latest
        ports:
        - containerPort: 3000
        env:
        - name: REACT_APP_API_URL
          value: "https://notes.ksaurav.com.np/api"
        - name: REACT_APP_ENVIRONMENT
          value: "production"

frontend-service


apiVersion: v1
kind: Service
metadata:
  name: notes-frontend-service
  namespace: notes-maker
spec:
  selector:
    app: notes-frontend
  ports:
  - port: 3000
    targetPort: 3000
    protocol: TCP
  type: ClusterIP

lets-encrypt:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
  namespace: notes-maker
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: saurav@gmail.com
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
    - http01:
        ingress:
          class: traefik

notes-ingress

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: notes-app-ingress
  namespace: notes-maker
  annotations:
    traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
  - hosts:
    - notes.ksaurav.com.np
    secretName: notes-tls-secret
  rules:
  - host: notes.ksaurav.com.np
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: notes-frontend-service
            port:
              number: 3000
      - path: /api
        pathType: Prefix
        backend:
          service:
            name: notes-backend-service
            port:
              number: 8080

For containerization: docker-compose:

version: '3.8'
services:
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      - MICRONAUT_SERVER_HOST=0.0.0.0
      - MICRONAUT_SERVER_PORT=8080
      - CASSANDRA_DEFAULT_CONTACT_POINTS=scylladb
      - CASSANDRA_DEFAULT_PORT=9042
      - CASSANDRA_DEFAULT_DATACENTER=datacenter1
      - CASSANDRA_DEFAULT_KEYSPACE=notes_maker
      - LOG_LEVEL=DEBUG
    depends_on:
      scylladb:
        condition: service_healthy
    networks:
      - notes_maker_network
    restart: on-failure

  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - REACT_APP_API_URL=http://172.64.36.6:8080/api
      - REACT_APP_ENVIRONMENT=development
    depends_on:
      - backend
    networks:
      - notes_maker_network

  scylladb:
    image: scylladb/scylla:5.4
    ports:
      - "9042:9042"
    volumes:
      - scylla_data:/var/lib/scylla
    networks:
      - notes_maker_network
    healthcheck:
      test: ["CMD-SHELL", "cqlsh -e 'SELECT now() FROM system.local;' || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 10
      start_period: 60s
    environment:
      - SCYLLA_CLUSTER_NAME=notes_maker_cluster       
volumes:
  scylla_data:

networks:
  notes_maker_network:
    driver: bridge
0
Subscribe to my newsletter

Read articles from Saurav Karki directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Saurav Karki
Saurav Karki