Spring Cloud Config Server: Centralized Configuration for Microservices
Master Spring Cloud Config Server with Git backend, @RefreshScope, Spring Cloud Bus, {cipher} encryption, and Vault integration for production microservices.
- @EnableConfigServer turns a Spring Boot app into a config server backed by Git, filesystem, or Vault
- Clients pull config at startup via spring.config.import=configserver: and override with profile-specific files like app-dev.yml
- @RefreshScope beans reload at runtime when /actuator/refresh is POST-ed without restarting the JVM
- Spring Cloud Bus broadcasts refresh events across all instances using RabbitMQ or Kafka, eliminating per-pod curl calls
- Sensitive values are stored as {cipher}... in Git and decrypted by the server using symmetric or RSA keys
Think of Spring Cloud Config Server as a central settings book kept in a locked filing cabinet (Git). Every microservice is like a new employee who looks up their settings from that book on their first day. If the manager updates the book (pushes a commit), Spring Cloud Bus is the office PA system that tells every employee simultaneously — no one has to restart or re-read the whole book, just the pages that changed.
You are debugging a production outage at 2 AM. The database connection pool is exhausted. The fix is a one-line property change, but your microservices architecture has 40 pods across 12 services. Rolling restart will take 20 minutes and risk dropping in-flight transactions. If only you could push a config change and have it applied everywhere in seconds — without a restart. That is exactly the problem Spring Cloud Config Server was built to solve.
In a monolith, externalized configuration is easy: one application.properties file, one JVM, one restart. But microservices shatter that simplicity. You now have dozens of services, each with its own config files, often duplicating the same database URLs, API keys, and feature flags. Drift is inevitable. A developer changes a property in one service and forgets the five others that share it. By next quarter, you have a configuration archaeology problem.
Spring Cloud Config Server centralizes all configuration in a single Git repository (or Vault, or a filesystem). Every microservice becomes a config client that fetches its properties at startup. The server resolves configurations by application name, Spring profile, and label (Git branch or tag), giving you per-environment, per-service configuration with a complete audit trail courtesy of Git history.
Beyond centralization, the real power is dynamic refresh. @RefreshScope marks beans so their dependencies can be re-injected without restarting the JVM. POST /actuator/refresh on a single pod refreshes it. Spring Cloud Bus — a thin event bus on top of RabbitMQ or Kafka — broadcasts that refresh event to every pod at once, making fleet-wide config changes a sub-second operation.
Encryption is the other production concern that Config Server solves elegantly. Plaintext credentials in Git repositories are an audit failure waiting to happen. Config Server's {cipher} prefix lets you store AES-encrypted or RSA-encrypted values in Git and have the server decrypt them transparently before serving clients. Alternatively, Vault backend gives you full secrets management with dynamic credentials, lease rotation, and fine-grained access policies.
This guide covers every layer of Spring Cloud Config Server production deployment: Git backend configuration, client bootstrap, profile-specific overrides, @RefreshScope internals, Spring Cloud Bus setup, encryption key management, Vault backend, and the failure modes that trip up senior engineers in production.
Setting Up the Config Server with Git Backend
A Config Server is a plain Spring Boot application with one annotation and a dependency. Add spring-cloud-config-server to your pom.xml, annotate the main class with @EnableConfigServer, and configure the Git backend. The minimal setup is deceptively simple, but production hardening requires attention to clone-on-start, timeout, basedir, and retry configuration.
The Git backend clones the remote repository to a local basedir on the server. By default, this clone happens lazily on the first request, meaning the first client to start after a server deployment bears the clone latency and any Git authentication failures surface only then. Set spring.cloud.config.server.git.clone-on-start=true to fail fast at startup.
Authentication with private repositories is a common stumbling block. HTTPS authentication uses spring.cloud.config.server.git.username and spring.cloud.config.server.git.password (or a token). SSH authentication requires either mounting a private key file and configuring spring.cloud.config.server.git.private-key, or using the ignoreLocalSshSettings approach with an inline key. Always add the Git host to known_hosts or set strict-host-key-checking=false in non-critical environments.
Search paths let you organize configs in subdirectories. spring.cloud.config.server.git.search-paths={application} allows each service to have its own directory, keeping the repository organized as your service count grows. The {application}, {profile}, and {label} placeholders are available in both search-paths and uri configurations, enabling per-service repository strategies if you need stricter isolation.
For high availability, run multiple Config Server instances behind a load balancer. Since the Git backend uses a local clone, configure a shared volume or — better — accept that each instance will maintain its own clone. Clients should configure spring.cloud.config.fail-fast=true with retry: initialInterval, maxAttempts, and multiplier to handle transient Config Server unavailability without crashing at startup. With multiple config server instances registered in Eureka, clients can use service discovery to locate the config server, removing the need for a hardcoded URL.
The server exposes configuration at /{application}/{profiles}/{label}. The label defaults to main (or master) but can be overridden per-client with spring.cloud.config.label=release/1.2, enabling blue-green config deployments where new config is tested on the release branch before being merged to main.
@RefreshScope and Runtime Configuration Updates
@RefreshScope is one of the most powerful — and most misunderstood — features of Spring Cloud Config. It creates a special Spring scope where beans are re-initialized when a refresh event is triggered. Under the hood, Spring wraps @RefreshScope beans in a CGLIB proxy. The proxy stores the bean instance in a RefreshScope cache. When POST /actuator/refresh is called, Spring Cloud Context clears this cache and re-fetches all @Value and @ConfigurationProperties from the updated Environment. The next method call on the proxy triggers re-initialization of the actual bean instance.
The critical subtlety is that the proxy only works when the bean is accessed through Spring's proxy mechanism — i.e., when another bean calls a method on the injected reference. If a singleton bean stores a hard reference to the concrete unwrapped instance (which can happen with constructor injection in certain patterns), the proxy chain is broken and refresh has no effect on that singleton's view.
For @ConfigurationProperties beans, the behavior is slightly different. If the @ConfigurationProperties class is not @RefreshScope, it will not be re-bound on refresh. However, if it is @RefreshScope, Spring will re-bind the entire properties object. This works well for configuration classes used via property accessors (getters) rather than stored field values.
The /actuator/refresh endpoint returns a JSON array of changed keys — use this in your CI/CD pipeline to log which configuration changed and verify the intended keys appear. If the array is empty after a legitimate config push, the Config Server may still be serving a cached Git clone — check spring.cloud.config.server.git.refresh-rate or manually hit /actuator/refresh on the Config Server itself first.
For Spring WebFlux (reactive) applications, @RefreshScope works the same way but be careful with reactive pipelines that capture configuration at assembly time (e.g., a Flux built from a configurable duration). These do not automatically see refreshed values because the pipeline is assembled once. Move configuration reads inside the reactive operators (e.g., Mono.fromSupplier(() -> configBean.getTimeout())) so each subscription picks up current values.
Spring Cloud Bus: Fleet-Wide Configuration Broadcast
Calling POST /actuator/refresh on every pod manually is impractical at scale. Spring Cloud Bus solves this by routing refresh events through a shared message broker — RabbitMQ or Kafka — so a single trigger refreshes every connected instance simultaneously. The bus integration adds two endpoints to the Config Server: /actuator/busrefresh (refresh all instances) and /actuator/busrefresh/{destination} (refresh a specific service:instance).
The canonical production flow is: developer pushes config to Git → Git webhook calls POST /actuator/busrefresh on the Config Server → Config Server publishes a RefreshRemoteApplicationEvent to the bus topic → all microservices subscribed to the topic receive the event → each instance calls its local /actuator/refresh logic → @RefreshScope beans reload their @Value fields.
Setting up Spring Cloud Bus requires adding spring-cloud-starter-bus-amqp (RabbitMQ) or spring-cloud-starter-bus-kafka to every service that needs to participate, including the Config Server itself. All services must connect to the same broker. The default exchange/topic is named springCloudBus for AMQP and springCloudBus for Kafka.
For Kafka-backed Bus, configure the default topic: spring.cloud.bus.destination=springCloudBus. Each application instance creates a consumer group based on its spring.application.name and instance index, ensuring each pod receives its own copy of the event (broadcast semantics, not work-queue semantics). Verify this with kafka-consumer-groups.sh — you should see one consumer group per service name, not one shared group.
Webhook security is critical. Your Git provider (GitHub, GitLab, Bitbucket) will send a POST request to your Config Server's public endpoint. Protect this endpoint with a shared secret header or IP allowlist. Spring Security can restrict /actuator/busrefresh to requests bearing a specific token. Alternatively, put the Config Server behind an API gateway and configure the webhook target to the gateway with authentication.
Encrypting Secrets with {cipher} and Vault Backend
Storing plaintext credentials in a Git repository is a security anti-pattern that violates SOC 2, PCI DSS, and most corporate security policies. Spring Cloud Config Server provides two solutions: built-in {cipher} encryption using AES or RSA, and full HashiCorp Vault backend integration.
For {cipher} encryption, the Config Server exposes /encrypt and /decrypt endpoints. You POST a plaintext value to /encrypt and receive an encrypted hex string. Store this in your Git repo as {cipher}EncryptedHexString. When serving this property to a client, the server automatically decrypts it before including it in the response. Clients never see or handle encrypted values — they receive plaintext, so no client-side changes are needed.
Symmetric encryption uses a single key configured via encrypt.key (or the ENCRYPT_KEY environment variable). RSA encryption uses a keystore configured with encrypt.keyStore.location, .alias, .password, and .secret. RSA is preferred in regulated environments because it allows you to rotate the private key without re-encrypting all values, and you can distribute the public key to teams who need to encrypt new secrets without giving them decryption access.
Vault backend is the enterprise-grade alternative. Configure spring.cloud.config.server.vault.* to point Config Server at your Vault cluster. Services can authenticate with their Vault tokens, AppRole credentials, or Kubernetes service account JWTs. Vault provides dynamic secrets (short-lived database credentials generated on demand), automatic lease renewal, and detailed audit logs of every secret access — capabilities that static {cipher} encryption cannot match.
For composite backends, combine Git (non-secret config) with Vault (secrets) using the composite repository configuration. The Config Server merges property sources from both backends before serving clients, so services see a unified environment without knowing that some properties came from Vault.
Key management is the operational burden with {cipher} encryption. When you rotate the encryption key, all existing {cipher} values must be re-encrypted. Establish a runbook: decrypt all values with the old key, re-encrypt with the new key, commit, verify, then update the server's key configuration. Vault solves this with lease rotation, making it the preferred choice for mature production deployments.
Profile-Specific Configuration and Hierarchical Overrides
Spring Cloud Config Server resolves configurations in a layered hierarchy that mirrors Spring's standard property source precedence. Understanding this hierarchy prevents the common confusion of why a value is not what you expect.
The resolution order (highest to lowest precedence) for a request to /payment-service/prod is: payment-service-prod.yml > payment-service.yml > application-prod.yml > application.yml. The server merges these files and returns a unified property source. This lets you define global defaults in application.yml (shared across all services), service-wide defaults in payment-service.yml, environment overrides in application-prod.yml, and service-plus-environment specific values in payment-service-prod.yml.
A common pattern is to keep shared infrastructure configuration (like logging levels, actuator settings, tracing configuration) in application.yml and let each service own only what differs. This dramatically reduces duplication. When you want to change the global log level from INFO to DEBUG for a production incident, one commit to application.yml + one busrefresh propagates to the entire fleet.
Profile activation on the client side follows normal Spring rules: spring.profiles.active=prod in application.yml or the SPRING_PROFILES_ACTIVE environment variable. In Kubernetes, set this as an environment variable in the Deployment spec so the same Docker image behaves differently per environment without rebuilding.
Label-based configuration (mapping to Git branches or tags) enables config promotion workflows. Maintain a config/dev branch for development, config/staging for staging, and config/prod for production. Clients in each environment set spring.cloud.config.label=config/prod accordingly. Promoting config to production becomes a Git merge with a pull request review — exactly the same workflow used for code changes, complete with diff visibility and approval gates.
Pattern-based repository selection allows routing different services to different Git repositories: spring.cloud.config.server.git.repos.payment.uri and spring.cloud.config.server.git.repos.payment.pattern=payment- routes all services matching payment- to a dedicated repository. This is useful for compliance isolation — keeping PCI-scoped services' configuration in a repository with stricter access controls.
High Availability and Failure Modes
Config Server is a critical dependency for all microservices that read remote configuration at startup. A Config Server outage during a rolling deployment means new pods fail to start, potentially degrading service capacity. Designing Config Server for high availability is not optional in production.
The simplest HA strategy is running multiple Config Server instances behind a load balancer. Since Config Server is stateless (it reads from Git and serves HTTP), horizontal scaling is straightforward. Register Config Server instances in Eureka and configure clients with spring.cloud.config.discovery.enabled=true so they locate the server through service discovery. This eliminates the hardcoded config server URL and allows zero-downtime Config Server rolling updates.
Git connectivity is the primary failure mode. If the Git host is unreachable, the Config Server cannot fetch updated config but can still serve from its local clone (the basedir cache). This behavior is configurable — by default, the server falls back to the local cache if Git is unreachable. This means a transient Git outage does not prevent clients from starting, but they will receive the last-known-good config rather than the latest. For disaster recovery, size the basedir persistent volume to hold the full repository.
Client-side resilience requires spring.cloud.config.fail-fast=true with retry configuration. Without fail-fast, a client will start with no remote config and use only local application.yml defaults — potentially in a broken state with missing required properties. With fail-fast=true, the client retries the config server connection with exponential backoff and fails the startup if it cannot connect after the configured attempts. This is the safer behavior in production.
For truly critical services, consider caching the last-received config locally in a ConfigMap or file so the service can start even if Config Server is completely unavailable. Spring Boot's file-based property source or environment variables can serve as a last-resort fallback. Some teams implement a sidecar that periodically writes config snapshots to a local file, which the main container reads as a fallback source.
Health monitoring should include a custom HealthIndicator that periodically verifies the Config Server can reach Git. Expose this via /actuator/health and alert on failure. Combine this with synthetic monitoring that POSTs to /actuator/refresh every 5 minutes to detect silent Git connectivity failures before they affect deployments.
Silent Stale Config: Why @RefreshScope Did Not Refresh
- @RefreshScope only works when the refreshed bean is accessed through its Spring proxy at runtime — never through a stored concrete reference inside a singleton.
- Always audit the call chain: if a @RefreshScope bean is injected into a singleton, the refresh will silently do nothing for that singleton's view of the bean.
curl -s http://config-server:8888/my-service/prod | jq '.propertySources[].name'curl -s http://my-service:8080/actuator/env | jq '.propertySources[] | select(.name | startswith("configserver"))'Key takeaways
Common mistakes to avoid
7 patternsNot setting clone-on-start=true
Injecting @RefreshScope beans into singletons via constructor
Exposing /actuator/busrefresh without authentication
Using fail-fast=false (the default) in production
Storing unencrypted secrets in application-prod.yml in Git
Running only one Config Server instance
Not setting refresh-rate on the Git backend
Interview Questions on This Topic
What does @EnableConfigServer do and what are the minimal requirements to make it functional?
Frequently Asked Questions
That's Spring Cloud. Mark it forged?
11 min read · try the examples if you haven't