
El problema: una aplicación, varios entornos
Toda aplicación Spring Boot real se ejecuta en al menos tres entornos: el portátil del desarrollador (dev), el cluster de pruebas o staging (staging) y producción (prod). Cada uno necesita configuración distinta:
- dev: H2 in-memory o PostgreSQL local en Docker, SMTP fake, logging en DEBUG, endpoints de Actuator todos abiertos, sin TLS.
- staging: PostgreSQL del servidor staging, SMTP de Mailtrap, logging en INFO, métricas a Prometheus interno, datos sintéticos.
- prod: PostgreSQL en HA con réplica, SMTP corporativo, logging en WARN con JSON estructurado, Actuator restringido, TLS, secretos de Vault.
Hardcodear estas diferencias en código o pasarlas como argumentos en cada despliegue es frágil y poco mantenible. Spring Boot resuelve esto con profiles y un sistema de propiedades externalizadas en capas.
Anatomía de un proyecto multi-entorno
La estructura habitual en src/main/resources/:
application.yml # Propiedades comunes a todos los entornos
application-dev.yml # Solo cuando el perfil "dev" está activo
application-staging.yml # Solo cuando "staging" está activo
application-prod.yml # Solo cuando "prod" está activo
application-test.yml # Solo cuando "test" está activo (tests JUnit)
El archivo base application.yml contiene lo común y referencias a placeholders que se rellenan según el perfil:
# application.yml
spring:
application:
name: empleados-api
jpa:
open-in-view: false
hibernate:
ddl-auto: validate
server:
port: 8080
shutdown: graceful
management:
endpoints:
web:
exposure:
include: health, info, metrics, prometheus
logging:
level:
root: INFO
application-dev.yml sobreescribe lo que cambia en dev:
# application-dev.yml
spring:
datasource:
url: jdbc:h2:mem:empleados;DB_CLOSE_DELAY=-1
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
h2:
console:
enabled: true
logging:
level:
com.empresa.empleados: DEBUG
org.springframework.web: DEBUG
application-prod.yml define lo que cambia en producción, con secretos referenciados por variables de entorno:
# application-prod.yml
spring:
datasource:
url: jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=require
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 20
connection-timeout: 30000
management:
endpoints:
web:
exposure:
include: health, info, prometheus
endpoint:
health:
show-details: never
logging:
level:
root: WARN
com.empresa.empleados: INFO
pattern:
console: '{"timestamp":"%d{yyyy-MM-dd HH:mm:ss.SSS}","level":"%level","logger":"%logger","message":"%message"}%n'
Los placeholders ${DB_HOST} se resuelven desde variables de entorno del contenedor o pod. Producción nunca contiene secretos en el archivo .yml que se commitea; solo placeholders.
Activar un perfil
Por argumento de línea de comandos
java -jar empleados-api.jar --spring.profiles.active=prod
Por variable de entorno
SPRING_PROFILES_ACTIVE=prod java -jar empleados-api.jar
En Docker:
ENV SPRING_PROFILES_ACTIVE=prod
En Kubernetes:
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
En tiempo de test
@SpringBootTest
@ActiveProfiles("test")
class EmpleadoIntegrationTest {
// ...
}
Por defecto en application.yml
spring:
profiles:
active: dev
Anti-patrón: poner
spring.profiles.active=prodpor defecto enapplication.yml. Si el archivo se commitea con ese valor, una ejecución accidental sin variable de entorno arrancará en modo prod en local. Lo idiomático es default a dev y forzar el perfil en cada deploy.
Múltiples perfiles activos a la vez
Spring Boot admite varios perfiles activos. Útil para separar dimensiones (entorno vs feature flag):
--spring.profiles.active=prod,kafka-enabled,observability-full
Cada perfil carga su archivo application-{nombre}.yml. Si dos perfiles definen la misma propiedad, gana el último listado. Esto permite composiciones flexibles, pero hay que conocer la regla para evitar sorpresas.
Profile groups (Spring Boot 2.4+)
Cuando una arquitectura tiene perfiles compuestos, los profile groups evitan tener que listar cada uno:
# application.yml
spring:
profiles:
group:
production: prod, kafka-enabled, observability-full, audit-enabled
development: dev, kafka-disabled
staging: staging, kafka-enabled, observability-light
Con esto, activar --spring.profiles.active=production activa los cuatro perfiles del grupo. Útil para no contaminar la línea de comandos con listas largas y evitar olvidos.
@Profile sobre beans
A veces hace falta que un bean exista solo en ciertos perfiles. La anotación @Profile lo controla declarativamente:
@Configuration
public class EmailConfig {
@Bean
@Profile("dev | test")
public EmailService emailServiceFake() {
return new FakeEmailService(); // captura emails para inspeccionar
}
@Bean
@Profile("staging")
public EmailService emailServiceMailtrap() {
return new SmtpEmailService("smtp.mailtrap.io", 587);
}
@Bean
@Profile("prod")
public EmailService emailServiceCorporativo(
@Value("${corp.smtp.host}") String host,
@Value("${corp.smtp.port}") int port) {
return new SmtpEmailService(host, port);
}
}
La sintaxis dev | test significa "dev O test". También admite & (AND) y ! (NOT):
@Profile("prod & !maintenance")— prod y NO maintenance@Profile("!dev & !test")— ni dev ni test (cualquier otro)
Spring Boot 4 mantiene esta sintaxis. Es el método declarativo más limpio para activar diferentes implementaciones según entorno.
@Profile sobre clases @Configuration enteras
Para activar un conjunto coherente de beans:
@Configuration
@Profile("prod")
public class ProductionSecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.requiresChannel(ch -> ch.anyRequest().requiresSecure()) // forza HTTPS
// ...
.build();
}
}
En perfiles que no sean prod, esa clase no se carga y sus beans no existen. Útil para evitar configuraciones de producción en local.
@ConditionalOn... como alternativa más fina
@Profile es lo más usado, pero Spring tiene anotaciones condicionales más finas:
@ConditionalOnProperty(name = "feature.x.enabled", havingValue = "true")— activa el bean solo si una propiedad concreta estrue. Más granular que@Profile.@ConditionalOnMissingBean— solo si no hay otro bean del mismo tipo registrado.@ConditionalOnClass(name = "com.example.Foo")— solo si una clase está en el classpath.
Para feature flags, @ConditionalOnProperty es preferible a @Profile porque desacopla la activación del entorno:
@Bean
@ConditionalOnProperty(name = "feature.export-csv.enabled", havingValue = "true")
public CsvExporter csvExporter() {
return new CsvExporter();
}
# application.yml
feature:
export-csv:
enabled: false # apagado por defecto
# application-prod.yml
feature:
export-csv:
enabled: true # encendido en prod
Precedencia entre fuentes de propiedades
Spring Boot resuelve cada propiedad consultando fuentes en este orden (de mayor a menor prioridad):
flowchart TD
A[Argumentos linea comandos<br/>--spring.profiles.active=prod] --> R[Valor final]
B[Variables de entorno<br/>SPRING_DATASOURCE_URL] --> R
C[application-prod.yml] --> R
D[application.yml] --> R
E[Defaults de Spring Boot] --> R
A -.gana sobre.-> B
B -.gana sobre.-> C
C -.gana sobre.-> D
D -.gana sobre.-> E
Esto significa:
- Una variable de entorno siempre sobreescribe lo que pone el archivo
.yml. - Un archivo
application-prod.ymlsobreescribeapplication.ymlsolo si el perfilprodestá activo. - Un argumento
--spring.x.y=zgana sobre todo lo demás.
Implicación práctica: la única forma fiable de cambiar configuración en producción sin redeployar es vía variables de entorno. Forzar comportamientos con argumentos en línea de comandos es excepcional.
spring.config.import: traer configuración externa
A partir de Spring Boot 2.4 (y reforzado en 3.x y 4.x), spring.config.import permite cargar fuentes externas sin escribir código:
# application-prod.yml
spring:
config:
import:
- "vault://" # secretos desde HashiCorp Vault
- "configserver:http://config-server" # Spring Cloud Config Server
- "optional:configtree:/etc/secrets/" # archivos del filesystem (k8s secrets)
- "optional:file:/etc/empleados-api/" # archivos locales sobreescribibles
El prefijo optional: evita que la aplicación falle si la fuente no está disponible (útil en local cuando no hay Vault levantado).
configtree: lee cada archivo de un directorio como una propiedad: /etc/secrets/db.password → propiedad db.password. Es el patrón estándar para Kubernetes Secrets montados como volume.
@ConfigurationProperties + Profiles
Combinando profiles con @ConfigurationProperties (lección siguiente) se obtiene un patrón potente: una clase Java tipada que cambia automáticamente entre entornos.
@ConfigurationProperties(prefix = "app.email")
@Validated
public record EmailProperties(
@NotBlank String host,
@Min(1) @Max(65535) int port,
String username,
String password,
boolean tls
) {}
# application-dev.yml
app:
email:
host: localhost
port: 1025
tls: false
# application-prod.yml
app:
email:
host: ${SMTP_HOST}
port: ${SMTP_PORT:587}
username: ${SMTP_USERNAME}
password: ${SMTP_PASSWORD}
tls: true
El servicio recibe la configuración tipada por inyección:
@Service
public class EmailSenderImpl implements EmailService {
private final EmailProperties props;
public EmailSenderImpl(EmailProperties props) {
this.props = props;
}
@Override
public void enviar(String destinatario, String asunto, String cuerpo) {
var session = Session.getInstance(propsToJavaMail(props));
// ...
}
}
La validación con @Validated + Bean Validation impide que la aplicación arranque si una propiedad obligatoria falta. Ese fallo en arranque es siempre preferible a un NullPointerException en runtime tres horas después del deploy.
Buenas prácticas en proyectos enterprise
Ningún secreto en archivos commiteados
Los archivos application-*.yml se commitean. Los secretos no. La regla es: en application-prod.yml solo placeholders (${DB_PASSWORD}), nunca el valor real. Los valores reales vienen de variables de entorno, Vault, Consul o Kubernetes Secrets.
Un perfil por entorno + perfiles transversales
Estructura recomendada:
- Perfiles de entorno:
dev,staging,prod,test. Mutuamente exclusivos. - Perfiles transversales:
kafka-enabled,audit-enabled,observability-full. Combinables con cualquier entorno. - Profile groups:
development,production,stagingque agrupan los anteriores.
Default sensato
application.yml debe ser ejecutable por defecto en local sin variables de entorno. El perfil por defecto es dev y carga H2 in-memory o un docker-compose local.
Configuración tipada y validada
Cualquier propiedad usada en más de un sitio debe estar tipada con @ConfigurationProperties y validada con Bean Validation. La lección siguiente desarrolla este patrón en detalle.
Endpoints de Actuator restringidos en prod
En dev management.endpoints.web.exposure.include: '*' está bien. En prod solo health, info, prometheus con detalle restringido (management.endpoint.health.show-details: never).
Aplicar profiles bien hechos elimina la mayor parte de los incidentes de "funciona en mi máquina pero no en prod". Y deja la base para añadir luego configuración tipada, manejo de errores avanzado y observabilidad sin tocar código de negocio.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Spring Boot es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Spring Boot
Explora más contenido relacionado con Spring Boot y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Configurar perfiles dev, staging y prod con application-{env}.yml. Aplicar @Profile a beans, configuraciones y tests. Componer perfiles con profile groups. Importar configuración externa con spring.config.import. Comprender la precedencia entre command-line args, env vars, application.yml y profile-specific files.