Overview
The Invernaderos API is designed for production deployment on Kubernetes with StatefulSets for databases, horizontal scaling for the API, and persistent storage.
This guide assumes you have a Kubernetes cluster with cert-manager and Traefik Ingress Controller already installed.
Architecture
┌─────────────────────────────────────────────────────────┐
│ Traefik Ingress (TLS via cert-manager) │
│ inverapi-prod.apptolast.com │
└───────────────┬─────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────┐
│ API Deployment (2-3 replicas) │
│ - Spring Boot 3.5.7 │
│ - Java 21 LTS │
│ - Resource limits: 1 CPU, 2Gi RAM │
└───┬─────────┬──────────┬──────────┬───────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────┐ ┌────────┐ ┌────────┐ ┌─────┐
│TimDB│ │PgSQL │ │Redis │ │EMQX │
│5432 │ │5432 │ │6379 │ │1883 │
└─────┘ └────────┘ └────────┘ └─────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────┐
│ PersistentVolumes (HostPath) │
│ /mnt/k8s-storage/invernaderos/ │
│ - timescaledb/ │
│ - postgresql-metadata/ │
│ - redis/ │
└─────────────────────────────────────┘
Prerequisites
Kubernetes cluster v1.28+
kubectl configured with cluster access
cert-manager installed for TLS certificates
Traefik Ingress Controller
Storage path available: /mnt/k8s-storage/invernaderos/
DockerHub credentials (for private images)
Namespace Structure
apiVersion : v1
kind : Namespace
metadata :
name : apptolast-invernadero-api
labels :
app : invernaderos-api
environment : production
Persistent Storage Setup
Create storage directories on Kubernetes node
SSH to your Kubernetes node and create directories: sudo mkdir -p /mnt/k8s-storage/invernaderos/{timescaledb,postgresql-metadata,redis}
Set correct ownership
Databases run as user 999, Redis as user 1000: sudo chown -R 999:999 /mnt/k8s-storage/invernaderos/timescaledb
sudo chown -R 999:999 /mnt/k8s-storage/invernaderos/postgresql-metadata
sudo chown -R 1000:1000 /mnt/k8s-storage/invernaderos/redis
sudo chmod -R 755 /mnt/k8s-storage/invernaderos
Create PersistentVolumes
Apply PV manifests: kubectl apply -f k8s/03-storage/
apiVersion : v1
kind : PersistentVolume
metadata :
name : timescaledb-pv
spec :
capacity :
storage : 50Gi
accessModes :
- ReadWriteOnce
persistentVolumeReclaimPolicy : Retain
hostPath :
path : /mnt/k8s-storage/invernaderos/timescaledb
type : DirectoryOrCreate
---
apiVersion : v1
kind : PersistentVolumeClaim
metadata :
name : timescaledb-pvc
namespace : apptolast-invernadero-api
spec :
accessModes :
- ReadWriteOnce
resources :
requests :
storage : 50Gi
Database Deployments
TimescaleDB (Time-Series Database)
StatefulSet Configuration
04-timescaledb/statefulset.yaml
apiVersion : apps/v1
kind : StatefulSet
metadata :
name : timescaledb
namespace : apptolast-invernadero-api
spec :
serviceName : timescaledb
replicas : 1
selector :
matchLabels :
app : timescaledb
template :
metadata :
labels :
app : timescaledb
spec :
containers :
- name : timescaledb
image : timescale/timescaledb:latest-pg16
env :
- name : POSTGRES_DB
value : "greenhouse_timeseries"
- name : POSTGRES_USER
value : "admin"
- name : POSTGRES_PASSWORD
valueFrom :
secretKeyRef :
name : timescaledb-secret
key : password
- name : PGDATA
value : /var/lib/postgresql/data/pgdata
ports :
- containerPort : 5432
name : postgres
volumeMounts :
- name : data
mountPath : /var/lib/postgresql/data
resources :
requests :
memory : "1Gi"
cpu : "500m"
limits :
memory : "2Gi"
cpu : "1000m"
livenessProbe :
exec :
command :
- pg_isready
- -U
- admin
initialDelaySeconds : 30
periodSeconds : 10
readinessProbe :
exec :
command :
- pg_isready
- -U
- admin
initialDelaySeconds : 5
periodSeconds : 5
securityContext :
fsGroup : 999
runAsUser : 999
volumeClaimTemplates :
- metadata :
name : data
spec :
accessModes : [ "ReadWriteOnce" ]
resources :
requests :
storage : 50Gi
Service (NodePort for external access) :04-timescaledb/service.yaml
apiVersion : v1
kind : Service
metadata :
name : timescaledb
namespace : apptolast-invernadero-api
spec :
type : NodePort
selector :
app : timescaledb
ports :
- name : postgres
port : 5432
targetPort : 5432
nodePort : 30432 # External access at <node-ip>:30432
Connection Details :
Internal : timescaledb.apptolast-invernadero-api.svc.cluster.local:5432
External : <node-ip>:30432
Database : greenhouse_timeseries (prod) / greenhouse_timeseries_dev (dev)
Schema : iot
User : admin (from Secret)
PostgreSQL Metadata
StatefulSet Configuration
05-postgresql-metadata/statefulset.yaml
apiVersion : apps/v1
kind : StatefulSet
metadata :
name : postgresql-metadata
namespace : apptolast-invernadero-api
spec :
serviceName : postgresql-metadata
replicas : 1
selector :
matchLabels :
app : postgresql-metadata
template :
metadata :
labels :
app : postgresql-metadata
spec :
containers :
- name : postgresql
image : postgres:16-alpine
env :
- name : POSTGRES_DB
value : "postgres"
- name : POSTGRES_USER
value : "admin"
- name : POSTGRES_PASSWORD
valueFrom :
secretKeyRef :
name : postgresql-metadata-secret
key : password
- name : PGDATA
value : /var/lib/postgresql/data/pgdata
ports :
- containerPort : 5432
name : postgres
volumeMounts :
- name : data
mountPath : /var/lib/postgresql/data
resources :
requests :
memory : "512Mi"
cpu : "250m"
limits :
memory : "1Gi"
cpu : "500m"
securityContext :
fsGroup : 999
runAsUser : 999
volumeClaimTemplates :
- metadata :
name : data
spec :
accessModes : [ "ReadWriteOnce" ]
resources :
requests :
storage : 20Gi
Service :apiVersion : v1
kind : Service
metadata :
name : postgresql-metadata
namespace : apptolast-invernadero-api
spec :
type : NodePort
selector :
app : postgresql-metadata
ports :
- port : 5432
targetPort : 5432
nodePort : 30433
Connection Details :
Internal : postgresql-metadata.apptolast-invernadero-api.svc.cluster.local:5432
External : <node-ip>:30433
Database : postgres
Schema : metadata
Redis Cache
StatefulSet Configuration
06-redis/statefulset.yaml
apiVersion : apps/v1
kind : StatefulSet
metadata :
name : redis
namespace : apptolast-invernadero-api
spec :
serviceName : redis
replicas : 1
selector :
matchLabels :
app : redis
template :
metadata :
labels :
app : redis
spec :
containers :
- name : redis
image : redis:7-alpine
command :
- redis-server
- /etc/redis/redis.conf
ports :
- containerPort : 6379
name : redis
volumeMounts :
- name : data
mountPath : /data
subPath : redis
- name : config
mountPath : /etc/redis
resources :
requests :
memory : "512Mi"
cpu : "250m"
limits :
memory : "1Gi"
cpu : "500m"
volumes :
- name : config
configMap :
name : redis-config
securityContext :
fsGroup : 1000
runAsUser : 1000
volumeClaimTemplates :
- metadata :
name : data
spec :
accessModes : [ "ReadWriteOnce" ]
resources :
requests :
storage : 10Gi
ConfigMap (02-configmaps/redis-config.yaml):maxmemory 900mb
maxmemory-policy volatile-lru
save 300 10
save 60 10000
rdbcompression yes
requirepass ${REDIS_PASSWORD}
protected-mode yes
Service :apiVersion : v1
kind : Service
metadata :
name : redis
namespace : apptolast-invernadero-api
spec :
type : ClusterIP
selector :
app : redis
ports :
- port : 6379
targetPort : 6379
---
apiVersion : v1
kind : Service
metadata :
name : redis-nodeport
namespace : apptolast-invernadero-api
spec :
type : NodePort
selector :
app : redis
ports :
- port : 6379
targetPort : 6379
nodePort : 30379
Connection Details :
Internal : redis.apptolast-invernadero-api.svc.cluster.local:6379
External : <node-ip>:30379 (debugging only)
API Deployment
Production Deployment
10-api-prod/deployment.yaml
apiVersion : apps/v1
kind : Deployment
metadata :
name : invernaderos-api
namespace : apptolast-invernadero-api
spec :
replicas : 2 # High availability
selector :
matchLabels :
app : invernaderos-api
environment : production
template :
metadata :
labels :
app : invernaderos-api
environment : production
spec :
containers :
- name : api
image : apptolast/invernaderos-api:latest
imagePullPolicy : Always
env :
- name : SPRING_PROFILES_ACTIVE
value : "prod"
- name : JAVA_OPTS
value : "-Xms512m -Xmx1024m"
- name : TIMESCALE_PASSWORD
valueFrom :
secretKeyRef :
name : invernaderos-api-secret
key : timescale-password
- name : METADATA_PASSWORD
valueFrom :
secretKeyRef :
name : invernaderos-api-secret
key : metadata-password
- name : REDIS_HOST
value : "redis.apptolast-invernadero-api.svc.cluster.local"
- name : REDIS_PORT
value : "6379"
- name : REDIS_PASSWORD
valueFrom :
secretKeyRef :
name : invernaderos-api-secret
key : redis-password
- name : MQTT_BROKER_URL
value : "wss://mqttinvernaderoapi-ws.apptolast.com/mqtt"
- name : MQTT_USERNAME
valueFrom :
secretKeyRef :
name : invernaderos-api-secret
key : mqtt-username
- name : MQTT_PASSWORD
valueFrom :
secretKeyRef :
name : invernaderos-api-secret
key : mqtt-password
ports :
- containerPort : 8080
name : http
resources :
requests :
memory : "1Gi"
cpu : "500m"
limits :
memory : "2Gi"
cpu : "1000m"
livenessProbe :
httpGet :
path : /actuator/health/liveness
port : 8080
initialDelaySeconds : 60
periodSeconds : 10
timeoutSeconds : 5
failureThreshold : 3
readinessProbe :
httpGet :
path : /actuator/health/readiness
port : 8080
initialDelaySeconds : 30
periodSeconds : 5
timeoutSeconds : 3
failureThreshold : 3
Service :apiVersion : v1
kind : Service
metadata :
name : invernaderos-api
namespace : apptolast-invernadero-api
spec :
type : ClusterIP
selector :
app : invernaderos-api
environment : production
ports :
- name : http
port : 8080
targetPort : 8080
Ingress Configuration (Traefik)
10-api-prod/ingressroute.yaml
apiVersion : traefik.containo.us/v1alpha1
kind : IngressRoute
metadata :
name : invernaderos-api
namespace : apptolast-invernadero-api
spec :
entryPoints :
- websecure
routes :
- match : Host(`inverapi-prod.apptolast.com`)
kind : Rule
services :
- name : invernaderos-api
port : 8080
tls :
secretName : invernaderos-api-tls
---
apiVersion : cert-manager.io/v1
kind : Certificate
metadata :
name : invernaderos-api-tls
namespace : apptolast-invernadero-api
spec :
secretName : invernaderos-api-tls
issuerRef :
name : letsencrypt-prod
kind : ClusterIssuer
dnsNames :
- inverapi-prod.apptolast.com
Secrets Management
NEVER commit Secrets to version control! Use kubectl create secret or sealed secrets.
Create Secrets
01-secrets.yaml (Template)
# Generate secure passwords
TIMESCALE_PASSWORD = $( openssl rand -base64 32 )
METADATA_PASSWORD = $( openssl rand -base64 32 )
REDIS_PASSWORD = $( openssl rand -base64 32 )
MQTT_PASSWORD = $( openssl rand -base64 32 )
# Create secret
kubectl create secret generic invernaderos-api-secret \
--from-literal=timescale-password= "${ TIMESCALE_PASSWORD }" \
--from-literal=metadata-password= "${ METADATA_PASSWORD }" \
--from-literal=redis-password= "${ REDIS_PASSWORD }" \
--from-literal=mqtt-username= "greenhouse_api" \
--from-literal=mqtt-password= "${ MQTT_PASSWORD }" \
-n apptolast-invernadero-api
Deployment Process
Create namespace and secrets
kubectl apply -f k8s/00-namespace.yaml
kubectl apply -f k8s/01-secrets.yaml
kubectl apply -f k8s/02-configmaps/
Deploy storage
kubectl apply -f k8s/03-storage/
kubectl get pv,pvc -n apptolast-invernadero-api
Deploy databases
kubectl apply -f k8s/04-timescaledb/
kubectl apply -f k8s/05-postgresql-metadata/
kubectl apply -f k8s/06-redis/
# Wait for databases to be ready
kubectl wait --for=condition=ready pod -l app=timescaledb -n apptolast-invernadero-api --timeout=300s
kubectl wait --for=condition=ready pod -l app=postgresql-metadata -n apptolast-invernadero-api --timeout=300s
kubectl wait --for=condition=ready pod -l app=redis -n apptolast-invernadero-api --timeout=300s
Deploy API
kubectl apply -f k8s/10-api-prod/
# Watch deployment progress
kubectl rollout status deployment/invernaderos-api -n apptolast-invernadero-api
Verify deployment
# Check all resources
kubectl get all -n apptolast-invernadero-api
# Check certificate
kubectl get certificate -n apptolast-invernadero-api
# Test health endpoint
curl https://inverapi-prod.apptolast.com/actuator/health
Scaling and Updates
Horizontal Scaling
# Scale API to 3 replicas
kubectl scale deployment invernaderos-api --replicas=3 -n apptolast-invernadero-api
# Verify scaling
kubectl get pods -n apptolast-invernadero-api -l app=invernaderos-api
Rolling Updates
# Update to new image version
kubectl set image deployment/invernaderos-api \
api=apptolast/invernaderos-api:v2.0.0 \
-n apptolast-invernadero-api
# Watch rollout progress
kubectl rollout status deployment/invernaderos-api -n apptolast-invernadero-api
# Rollback if needed
kubectl rollout undo deployment/invernaderos-api -n apptolast-invernadero-api
Restart Deployment
# Restart all pods (e.g., after ConfigMap changes)
kubectl rollout restart deployment/invernaderos-api -n apptolast-invernadero-api
Monitoring and Logs
View Logs
# API logs
kubectl logs -f deployment/invernaderos-api -n apptolast-invernadero-api
# Database logs
kubectl logs -f statefulset/timescaledb -n apptolast-invernadero-api
kubectl logs -f statefulset/postgresql-metadata -n apptolast-invernadero-api
# Redis logs
kubectl logs -f statefulset/redis -n apptolast-invernadero-api
# Previous logs (if pod restarted)
kubectl logs --previous deployment/invernaderos-api -n apptolast-invernadero-api
Exec into Pods
# Connect to TimescaleDB
kubectl exec -it statefulset/timescaledb -n apptolast-invernadero-api -- psql -U admin -d greenhouse_timeseries
# Connect to PostgreSQL Metadata
kubectl exec -it statefulset/postgresql-metadata -n apptolast-invernadero-api -- psql -U admin -d postgres
# Connect to Redis
kubectl exec -it statefulset/redis -n apptolast-invernadero-api -- redis-cli -a "${ REDIS_PASSWORD }"
# Shell into API pod
kubectl exec -it deployment/invernaderos-api -n apptolast-invernadero-api -- /bin/sh
Resource Usage
# CPU and memory usage
kubectl top pods -n apptolast-invernadero-api
# Node resource usage
kubectl top nodes
Troubleshooting
Pod stuck in 'Pending' state
Cause : PVC not bound or insufficient resources.Solution :# Check PVC status
kubectl get pvc -n apptolast-invernadero-api
# Describe pod for events
kubectl describe pod < pod-nam e > -n apptolast-invernadero-api
# Check node resources
kubectl describe node < node-nam e >
Common issues:
Storage path doesn’t exist on node
Wrong ownership on storage directory
Node out of resources (CPU/memory)
Cause : Application failing to start.Solution :# View logs
kubectl logs < pod-nam e > -n apptolast-invernadero-api
# View previous logs
kubectl logs < pod-nam e > --previous -n apptolast-invernadero-api
# Check events
kubectl describe pod < pod-nam e > -n apptolast-invernadero-api
Common issues:
Missing environment variables
Database connection failed
Incorrect Secret values
Cause : cert-manager misconfiguration or DNS issue.Solution :# Check certificate status
kubectl describe certificate invernaderos-api-tls -n apptolast-invernadero-api
# Check cert-manager logs
kubectl logs -n cert-manager deployment/cert-manager -f
# Verify ClusterIssuer
kubectl get clusterissuer
kubectl describe clusterissuer letsencrypt-prod
Common issues:
DNS not pointing to cluster
ClusterIssuer not configured
Rate limit reached (Let’s Encrypt)
Cannot connect to database from API
Cause : Database not ready or wrong service name.Solution :# Check database pod status
kubectl get pods -n apptolast-invernadero-api -l app=timescaledb
# Test connectivity from API pod
kubectl exec -it deployment/invernaderos-api -n apptolast-invernadero-api -- \
sh -c 'nc -zv timescaledb.apptolast-invernadero-api.svc.cluster.local 5432'
# Check service endpoints
kubectl get endpoints -n apptolast-invernadero-api
Backup and Disaster Recovery
Database Backups
# TimescaleDB backup
kubectl exec -it statefulset/timescaledb -n apptolast-invernadero-api -- \
pg_dump -U admin greenhouse_timeseries > backup_timescale_ $( date +%Y%m%d ) .sql
# PostgreSQL Metadata backup
kubectl exec -it statefulset/postgresql-metadata -n apptolast-invernadero-api -- \
pg_dump -U admin postgres > backup_metadata_ $( date +%Y%m%d ) .sql
# Redis backup (RDB snapshot)
kubectl exec -it statefulset/redis -n apptolast-invernadero-api -- \
redis-cli -a "${ REDIS_PASSWORD }" BGSAVE
Restore from Backup
# Restore TimescaleDB
cat backup_timescale_20250116.sql | \
kubectl exec -i statefulset/timescaledb -n apptolast-invernadero-api -- \
psql -U admin greenhouse_timeseries
# Restore PostgreSQL Metadata
cat backup_metadata_20250116.sql | \
kubectl exec -i statefulset/postgresql-metadata -n apptolast-invernadero-api -- \
psql -U admin postgres
CI/CD Integration
GitHub Actions workflow (.github/workflows/build-and-push.yml):
name : Build and Push Docker Image
on :
push :
branches :
- main
- develop
jobs :
build :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v3
- name : Set up JDK 21
uses : actions/setup-java@v3
with :
java-version : '21'
distribution : 'temurin'
- name : Build with Gradle
run : ./gradlew build -x test
- name : Login to DockerHub
uses : docker/login-action@v2
with :
username : ${{ secrets.DOCKERHUB_USERNAME }}
password : ${{ secrets.DOCKERHUB_TOKEN }}
- name : Build and push
uses : docker/build-push-action@v4
with :
context : .
push : true
tags : |
apptolast/invernaderos-api:${{ github.ref_name }}
apptolast/invernaderos-api:latest
After image is pushed, update Kubernetes:
# Force pull new image
kubectl rollout restart deployment/invernaderos-api -n apptolast-invernadero-api
Next Steps
Configuration Configure environment variables and application settings
Monitoring Set up Prometheus and Grafana dashboards
High Availability Configure database replication and load balancing
Security Implement security best practices