Overview
The Invernaderos API uses Spring Boot profiles and environment variables for configuration across different environments (local, dev, prod).
Configuration follows the 12-factor app methodology with externalized configuration and secrets management.
Configuration Structure
src/main/resources/
├── application-dev.yaml # Development profile
├── application-prod.yaml # Production profile
└── db/migration/ # Flyway migrations
Spring Profiles
Development Profile (dev)
View application-dev.yaml
# =============================================================================
# PERFIL DEV - Configuración para desarrollo local y entorno de desarrollo
# =============================================================================
# Activar con: --spring.profiles.active=dev
# O variable de entorno: SPRING_PROFILES_ACTIVE=dev
# =============================================================================
# Flyway: Auto-migrate habilitado para desarrollo rápido
flyway :
auto-migrate : true
# Simulación habilitada para desarrollo sin sensores físicos
greenhouse :
simulation :
enabled : true
# Logging más detallado para debugging
logging :
level :
org.flywaydb : DEBUG
com.apptolast.invernaderos : DEBUG
Key Features :
Auto-migration : Database migrations run automatically on startup
Simulation mode : Generates fake sensor data every 5 seconds
Debug logging : Detailed logs for development and troubleshooting
When to use :
Local development with Docker Compose
Development Kubernetes namespace
Testing without physical sensors
Production Profile (prod)
View application-prod.yaml
# =============================================================================
# PERFIL PROD - Configuración para producción
# =============================================================================
# Activar con: --spring.profiles.active=prod
# O variable de entorno: SPRING_PROFILES_ACTIVE=prod
# =============================================================================
#
# IMPORTANTE - ESTRATEGIA DE MIGRACIONES EN PROD:
# Las migraciones NO se ejecutan automáticamente al arrancar la aplicación.
# Deben ejecutarse via CI/CD ANTES del deploy usando:
#
# flyway -url=jdbc:postgresql://host:port/greenhouse_metadata \
# -user=admin -password=xxx \
# -schemas=metadata \
# -locations=classpath:db/migration \
# migrate
#
# Documentación: https://documentation.red-gate.com/fd/recommended-practices-150700352.html
# =============================================================================
# Flyway: Solo VALIDATE, NO auto-migrate
# Las migraciones se ejecutan via CI/CD antes del deploy
flyway :
auto-migrate : false
# Simulación DESHABILITADA en producción (usar sensores reales)
greenhouse :
simulation :
enabled : false
# Logging optimizado para producción
logging :
level :
root : INFO
org.flywaydb : INFO
com.apptolast.invernaderos : INFO
org.hibernate.SQL : WARN
Key Features :
Manual migrations : Migrations run via CI/CD before deployment (safer)
Simulation disabled : Uses real sensor data from MQTT
Production logging : INFO level to reduce log volume
When to use :
Production Kubernetes deployment
Staging environment
Any environment with real sensors
Environment Variables
Database Configuration
TimescaleDB (Time-Series Database)
TIMESCALE_DB_NAME = greenhouse_timeseries
TIMESCALE_USER = admin
TIMESCALE_PASSWORD =< secure_password >
Connection URL :
Docker : jdbc:postgresql://timescaledb:5432/greenhouse_timeseries
Kubernetes : jdbc:postgresql://timescaledb.apptolast-invernadero-api.svc.cluster.local:5432/greenhouse_timeseries
Schema : iot
Tables :
sensor_readings - Hypertable with 7-day chunks
sensor_readings_hourly - Continuous aggregate (hourly stats)
sensor_readings_daily_by_tenant - Continuous aggregate (daily tenant stats)
Configuration Details :
spring :
datasource :
timescale :
jdbc-url : jdbc:postgresql://${TIMESCALE_HOST:localhost}:${TIMESCALE_PORT:5432}/${TIMESCALE_DB_NAME:greenhouse_timeseries}
username : ${TIMESCALE_USER:admin}
password : ${TIMESCALE_PASSWORD}
driver-class-name : org.postgresql.Driver
hikari :
pool-name : TimescaleHikariPool
maximum-pool-size : 20
minimum-idle : 5
connection-timeout : 30000
idle-timeout : 600000
max-lifetime : 1800000
PostgreSQL Metadata
METADATA_DB_NAME = greenhouse_metadata
METADATA_USER = admin
METADATA_PASSWORD =< secure_password >
Connection URL :
Docker : jdbc:postgresql://postgresql-metadata:5432/postgres
Kubernetes : jdbc:postgresql://postgresql-metadata.apptolast-invernadero-api.svc.cluster.local:5432/postgres
Schema : metadata
Tables :
tenants - Multi-tenant isolation
greenhouses - Greenhouse configurations
sensors, actuators - Device registry
users - User management
alerts - Alert history
mqtt_users - MQTT authentication
Configuration Details :
spring :
datasource :
metadata :
jdbc-url : jdbc:postgresql://${METADATA_HOST:localhost}:${METADATA_PORT:5432}/${METADATA_DB_NAME:postgres}
username : ${METADATA_USER:admin}
password : ${METADATA_PASSWORD}
driver-class-name : org.postgresql.Driver
hikari :
pool-name : MetadataHikariPool
maximum-pool-size : 10
minimum-idle : 2
connection-timeout : 30000
Redis Configuration
REDIS_HOST = redis
REDIS_PORT = 6379
REDIS_PASSWORD =< secure_password >
Data Structure : Sorted Set (ZSET)
Key : greenhouse:messages
TTL : 24 hours (renewed on each write)
Max Size : 1000 messages (oldest auto-evicted)
Configuration Details :
spring :
data :
redis :
host : ${REDIS_HOST:localhost}
port : ${REDIS_PORT:6379}
password : ${REDIS_PASSWORD}
database : 0
timeout : 60000ms
connect-timeout : 10000ms
client-type : lettuce
lettuce :
pool :
max-active : 100
max-idle : 50
min-idle : 10
max-wait : 3000ms
shutdown-timeout : 2000ms
cache :
type : redis
redis :
time-to-live : 600000 # 10 minutes
cache-null-values : false
key-prefix : "ts-app::"
use-key-prefix : true
Lettuce vs Jedis : The API uses Lettuce (async, reactive client) instead of Jedis for better performance and non-blocking I/O.
MQTT Configuration
Docker Compose
Kubernetes (Production)
MQTT_BROKER_URL = tcp://emqx:1883
MQTT_USERNAME =< mqtt_user >
MQTT_PASSWORD =< secure_password >
MQTT_CLIENT_ID_PREFIX = api_local_001
Topics :
GREENHOUSE/{tenantId} - Multi-tenant sensor data (e.g., GREENHOUSE/SARA)
GREENHOUSE - Legacy topic (maps to DEFAULT tenant)
GREENHOUSE/RESPONSE - Echo for verification
QoS Levels :
0 (At most once) : Sensor data (high frequency, loss acceptable)
1 (At least once) : Actuator commands (delivery important)
2 (Exactly once) : Critical alerts (no duplicates)
Configuration Details :
mqtt :
broker :
url : ${MQTT_BROKER_URL:tcp://localhost:1883}
username : ${MQTT_USERNAME}
password : ${MQTT_PASSWORD}
client-id-prefix : ${MQTT_CLIENT_ID_PREFIX:api_default}
inbound :
topics :
- GREENHOUSE
- GREENHOUSE/#
- SENSOR/#
- ACTUATOR/#
qos : 0
clean-session : false
outbound :
qos : 0
retained : false
WebSocket Secure (WSS) : Production uses wss:// for encrypted MQTT over WebSocket. Development uses tcp:// for simplicity.
Flyway Migration Settings
Development (Auto-Migrate)
flyway :
auto-migrate : true
schemas :
- metadata # PostgreSQL Metadata
- iot # TimescaleDB (manual connection)
locations :
- classpath:db/migration
baseline-on-migrate : true
validate-on-migrate : true
Behavior :
Migrations run automatically on startup
Creates schemas if they don’t exist
Validates checksums to detect manual changes
Production (Manual Migration via CI/CD)
flyway :
auto-migrate : false
Recommended Workflow :
Run migrations before deployment
Use Flyway CLI in CI/CD pipeline: flyway -url=jdbc:postgresql://timescaledb:5432/greenhouse_timeseries \
-user=admin -password=${ TIMESCALE_PASSWORD } \
-schemas=iot \
-locations=filesystem:./src/main/resources/db/migration \
migrate
flyway -url=jdbc:postgresql://postgresql-metadata:5432/postgres \
-user=admin -password=${ METADATA_PASSWORD } \
-schemas=metadata \
-locations=filesystem:./src/main/resources/db/migration \
migrate
Validate migrations on startup
Application validates that database schema matches expected state: flyway :
auto-migrate : false
validate-on-migrate : true # Fails if schema doesn't match
Deploy application
Only after successful migration, deploy the application.
Why manual migrations in prod? Prevents accidental schema changes, allows rollback testing, and provides audit trail via CI/CD logs.
Migration Files
Naming Convention : V{version}__{description}.sql
Examples:
V2__fix_composite_primary_key.sql
V8__uuid_migration_and_schema_change.sql
V11__create_staging_infrastructure_timescaledb.sql
Current Version : V11 (as of Nov 2025)
Migration Locations :
src/main/resources/db/migration/
├── V2__fix_composite_primary_key.sql
├── V3__add_tenant_company_fields.sql
├── V4__add_greenhouse_mqtt_fields.sql
├── V5__add_sensor_multi_tenant_fields.sql
├── V6__extend_actuators_table.sql
├── V7__migrate_existing_data_to_default_tenant.sql
├── V8__uuid_migration_and_schema_change.sql
├── V9__add_multi_tenant_indexes.sql
├── V10__extend_alerts_table.sql
└── V11__create_staging_infrastructure_timescaledb.sql
Security Configuration
Password Generation
OpenSSL
Generate All Passwords
# Generate 32-character base64 password
openssl rand -base64 32
Secrets Management
NEVER commit secrets to version control! Use:
.env files (local, gitignored)
Kubernetes Secrets (base64 encoded)
External secret managers (AWS Secrets Manager, HashiCorp Vault)
Docker Compose (.env file)
# .env (NEVER commit this file)
TIMESCALE_PASSWORD = abc123xyz...
METADATA_PASSWORD = def456uvw...
REDIS_PASSWORD = ghi789rst...
MQTT_PASSWORD = jkl012mno...
EMQX_DASHBOARD_PASSWORD = pqr345stu...
Kubernetes Secrets
Create Secret
Reference in Deployment
kubectl create secret generic invernaderos-api-secret \
--from-literal=timescale-password= "$( openssl rand -base64 32 )" \
--from-literal=metadata-password= "$( openssl rand -base64 32 )" \
--from-literal=redis-password= "$( openssl rand -base64 32 )" \
--from-literal=mqtt-username= "greenhouse_api" \
--from-literal=mqtt-password= "$( openssl rand -base64 32 )" \
-n apptolast-invernadero-api
Simulation Mode
Simulation mode generates realistic fake sensor data when physical sensors are unavailable.
Enable Simulation
greenhouse :
simulation :
enabled : true
greenhouse-id : "001"
interval : 5000 # milliseconds
Generated Data :
Temperature : 15-30°C with gradual variations
Humidity : 40-80% with gradual variations
Sectors : 12 sectors with random values
Extractors : 3 extractors with random states
Log Warning :
⚠️ SIMULATION MODE ENABLED - Generating fake greenhouse data
ALWAYS disable simulation in production! Set greenhouse.simulation.enabled: false in production profile.
JVM Configuration
Memory Settings
Docker Compose
Kubernetes (Production)
JAVA_OPTS = -Xms256m -Xmx512m
Recommendations :
Development : -Xms256m -Xmx512m
Production : -Xms512m -Xmx1024m (or higher based on load)
GC : G1GC for better pause times
Health Check Configuration
management :
endpoints :
web :
exposure :
include : health,info,metrics,prometheus
endpoint :
health :
probes :
enabled : true
show-details : when-authorized
health :
livenessState :
enabled : true
readinessState :
enabled : true
Endpoints :
/actuator/health - Overall health
/actuator/health/liveness - Kubernetes liveness probe
/actuator/health/readiness - Kubernetes readiness probe
/actuator/metrics - Micrometer metrics
/actuator/prometheus - Prometheus scraping endpoint
Profile Activation
Docker Compose
services :
api :
environment :
SPRING_PROFILES_ACTIVE : local
Kubernetes
env :
- name : SPRING_PROFILES_ACTIVE
value : "prod"
Command Line
# Gradle
./gradlew bootRun --args= '--spring.profiles.active=dev'
# JAR
java -jar app.jar --spring.profiles.active=prod
# Environment Variable
export SPRING_PROFILES_ACTIVE = dev
java -jar app.jar
Configuration Validation
Verify configuration on startup:
# Check loaded properties
curl http://localhost:8080/actuator/env
# Check active profile
curl http://localhost:8080/actuator/info
Expected response:
{
"app" : {
"name" : "Invernaderos API" ,
"version" : "0.0.1-SNAPSHOT" ,
"profiles" : [ "prod" ]
}
}
Next Steps
Docker Compose Deploy locally with Docker Compose
Kubernetes Deploy to production with Kubernetes
Database Migrations Manage Flyway database migrations
Security Security best practices