How to Migrate a Spring Boot Application from Cloud Foundry to Kubernetes (Without Rewriting It)
A step-by-step guide to moving a Spring Boot app off Cloud Foundry onto Kubernetes: what replaces buildpacks, VCAP_SERVICES, routes and cf scale, which code actually has to change, and how to keep the cf push experience your developers had.
Moving a Spring Boot app from Cloud Foundry to Kubernetes is a packaging and platform-contract change, not a code rewrite. Five things change, and your Java is usually none of them: how the image is built (CF buildpacks become Paketo Buildpacks, Jib or a Dockerfile), how config and credentials arrive (VCAP_SERVICES becomes Kubernetes Secrets and ConfigMaps), how traffic enters (CF routes become Ingress or the Gateway API), how you scale (cf scale -i becomes replicas plus an HPA), and how logs and health are exposed (Loggregator becomes stdout plus Actuator probes). Diego already ran your app in a container. You are swapping the platform around the same JAR, not containerizing from zero.
I have watched teams turn this into a six-month container-expertise project when it did not need to be one. This guide is the map I wish they had started with.
A Spring Boot app almost never needs a code rewrite to leave Cloud Foundry. The five things that change are image build, config and service bindings, routing, scaling, and logging plus health. The Java business logic travels unchanged.
The only code that usually changes is the Cloud Foundry glue: drop spring-cloud-connectors (now in maintenance mode), delete any VCAP_SERVICES parsing, drop the cloud profile, and bind to plain spring.datasource.* / spring.data.redis.* properties fed by environment variables. Do this refactor while you are still on Cloud Foundry, because env-var config behaves identically on both platforms.
Keep the no-Dockerfile workflow with Paketo Buildpacks and ./mvnw spring-boot:build-image. It is the same Cloud Native Buildpacks lineage Cloud Foundry adopted, with a JVM memory calculator that sizes heap from the container limit.
Expect platform work, not app work, to dominate the timeline. In my experience a single service moves in days once a cluster exists. A 20 to 50 app estate with shared service brokers is realistically one to two quarters, because routing, TLS, secrets, logging, quotas and zero-downtime deploys are now separate components you own.
There are four credible destinations: raw Kubernetes with Helm and Argo CD, Korifi (which reimplements cf push on Kubernetes), a vendor continuity path like Tanzu Platform or OpenShift, or an internal developer platform. Qovery is the last category and runs inside your own AWS, GCP, Azure or Scaleway account, so the cloud bill and the data stay in your name.
What actually changes when you move a Spring Boot app from Cloud Foundry to Kubernetes?
Five things change: image build, service bindings and config, routing, scaling, and logging plus health checks. Your Java code is usually not one of them. Cloud Foundry's Diego already ran your app as a container, so Kubernetes inherits a workload that is already containerizable, and the work is re-expressing the platform contract that CF gave you for free.
Here is the concept-by-concept mapping.
Cloud Foundry concept
Kubernetes / ecosystem equivalent
Spring Boot property or command
What you now own
Effort
cf push (build + deploy)
CI pipeline builds an image, then kubectl apply / GitOps
spring-boot:build-image in CI
The pipeline and the rollout
High
CF droplet
OCI container image
layered JAR in the image
Image build and storage
Medium
CF buildpack
Cloud Native Buildpacks (Paketo), Jib, or a Dockerfile
./mvnw spring-boot:build-image
Base image and CVE patching
Medium
CF route
Ingress or Gateway API HTTPRoute
server.port only
Routing, TLS, DNS
High
Service instance + service broker
Kubernetes Secret/ConfigMap or a managed cloud database
spring.datasource.*
Provisioning and credentials
High
cf scale -i 5
replicas: 5 plus an HPA
none
Autoscaling policy
Medium
CF org / space
Namespace plus RBAC
none
Access control model
Medium
CF space quota
ResourceQuota + LimitRange
none
Capacity guardrails
Medium
cf logs --recent
stdout to Loki, OpenSearch or Datadog
Logback to stdout
The logging pipeline
Medium
CF health check
liveness, readiness and startup probes
management.endpoint.health.probes.enabled
Probe tuning
Low
Why the Java mostly survives: Spring Boot already builds an executable JAR, already reads config from environment variables through relaxed binding (so SPRING_DATASOURCE_URL maps to spring.datasource.url with zero code change), and already exposes Actuator health groups that map one-to-one onto Kubernetes probes.
The real code-level exceptions are narrow and specific. Flag any app that uses spring-cloud-connectors or java-cfenv, parses VCAP_SERVICES or VCAP_APPLICATION directly, activates a cloud profile, reads CF_INSTANCE_INDEX or CF_INSTANCE_GUID, relies on CF sticky routing for in-memory sessions, or writes to the local filesystem.
One trap deserves a callout. Cloud Foundry's long-lived app containers quietly masked statefulness. Kubernetes reschedules pods far more aggressively, so anything on local disk has to move to object storage or a PersistentVolume, and in-memory HTTP sessions need Spring Session with Redis. A second quiet one: CF internal routes (apps.internal) and container networking policies become Kubernetes Service DNS (billing.default.svc.cluster.local) plus NetworkPolicy. Hardcoded apps.internal URLs are a frequent source of post-cutover 404s.
Do you have to rewrite your Spring Boot code to leave Cloud Foundry?
No. In the vast majority of cases you delete Cloud Foundry glue rather than rewrite business logic, and you can delete all of it while still deployed on Cloud Foundry. The changes are confined to dependencies, a few properties, and a handful of platform-aware classes.
Here is the deletion list, with the replacement for each:
spring-cloud-connectors -> standard Spring Boot auto-configuration. The project README states it is "in maintenance mode, in favor of the newer Java CFEnv project," which Spring announced back in 2019.
java-cfenv -> environment variables, or spring-cloud-bindings if you want binding semantics back.
Custom AbstractCloudConfig beans -> nothing at all, because Boot auto-configures the DataSource from spring.datasource.url.
Settle the version question deliberately. A large share of CF estates are still on Spring Boot 2.7.x, and 2.7 left OSS support on 2023-11-24. The current GA line in 2026 is Spring Boot 4.x, and most of the 3.x lines are already out of OSS support too. The jump from 2.x to 3.x carries the Jakarta namespace migration (javax.* to jakarta.*), which is a bigger change than the Kubernetes move itself. My advice: decouple the two. Move to Kubernetes on your current Boot version first, then schedule the framework upgrade as its own project. Doing both at once means you cannot tell which change broke things.
What you add, rather than rewrite, is small: management.endpoint.health.probes.enabled=true, structured JSON logging to stdout, and Micrometer's Prometheus registry at /actuator/prometheus. On Boot 2.x and 3.x you also add server.shutdown=graceful; on Boot 4.x that is already the default.
A concrete before and after. The Cloud Foundry version pulled the database out of VCAP_SERVICES in Java. The Kubernetes version is four lines of application.yaml and a Secret:
The honest exceptions that do need engineering: apps using the CF Autoscaler custom-metrics API, apps relying on CF route services for authentication, and anything using the CF Java client to introspect the platform at runtime. Those need a real redesign, not a property change.
How do you replace Cloud Foundry buildpacks when building the container image?
Use Paketo Buildpacks through ./mvnw spring-boot:build-image (or pack build). It is the same Cloud Native Buildpacks lineage Cloud Foundry adopted, donated to the CNCF by Pivotal and Heroku and graduated to the CNCF's top maturity tier in August 2026. You keep a zero-Dockerfile workflow with an automatic JRE, layered JARs and a JVM memory calculator. Jib and a hand-written Dockerfile are the two alternatives, in that order.
The exact command, with no Dockerfile in the repo:
The Spring Boot Maven plugin uses Paketo by default (the current default builder is paketobuildpacks/builder-noble-java-tiny), so this produces a layered OCI image with the same build logic CF used on your behalf.
Memory is the number one cause of post-migration OOMKills, so spend a paragraph on it. Paketo's Java memory calculator plays the role CF's buildpack played, deriving JVM memory from the container limit and tuning it through JAVA_TOOL_OPTIONS. You influence it with buildpack variables like BPL_JVM_THREAD_COUNT (default 250) and BPL_JVM_HEAD_ROOM. The gotcha appears when you skip the calculator and run a plain JDK image with only a Kubernetes memory limit: the JVM has had container awareness on by default since JDK 10, but its default MaxRAMPercentage is only 25 percent, so a 2Gi pod gives the heap roughly 512Mi and the process gets killed under load while the limit looks nowhere near full. Either let the memory calculator do its job, or set -XX:MaxRAMPercentage yourself.
Build option
Dockerfile required
JVM memory calculator
Layered image
Base image CVE ownership
Closest to cf push
Paketo Buildpacks (build-image)
No
Yes
Yes
Buildpack maintainers patch the base
Closest
Jib (jib-maven-plugin)
No, daemonless
No
Yes
You pick the base image
Close
Multi-stage Dockerfile
Yes
No
Manual
You own all patching
Furthest
Two more things you now own that CF hid. First, a registry: CF stored droplets for you, so now you choose ECR, Artifact Registry, ACR, Harbor or GHCR, plus image scanning and a retention policy. Second, layered JARs are worth keeping on by default because they cut image pull time and registry egress on every deploy, since only the changed application layer moves. GraalVM native images via the native buildpack are a later optimization for cold-start-sensitive services, not a migration prerequisite.
What replaces VCAP_SERVICES and Cloud Foundry service bindings in Kubernetes?
Replace VCAP_SERVICES with plain Spring Boot properties sourced from Kubernetes Secrets and ConfigMaps, and do that refactor before you leave Cloud Foundry. Environment-variable configuration behaves identically on both platforms, so moving to it on CF first removes the riskiest part of the cutover and keeps every step reversible.
Step 1 - bind to standard properties. Remove the connector dependency and point at spring.datasource.url, spring.datasource.username, spring.data.redis.host, spring.rabbitmq.addresses, spring.kafka.bootstrap-servers. These are the properties Boot already knows how to auto-configure.
Step 2 - inject them. Use envFrom with a secretRef for bulk injection, secretKeyRef for individual values, or mount the Secret as files and read it with spring.config.import=configtree:/etc/secrets/:
Step 3 - get CF-like binding semantics back, if you want them. Three options: the Service Binding Specification for Kubernetes, which projects each binding into a directory under SERVICE_BINDING_ROOT as the Kubernetes-native stand-in for VCAP_SERVICES; spring-cloud-bindings, which the Paketo Java buildpack contributes by default and which auto-configures PostgreSQL, MySQL, Redis, MongoDB, Kafka, RabbitMQ and roughly twenty other types from the bindings directory; or Spring Cloud Kubernetes for ConfigMap- and Secret-backed property sources with reload.
Two realities CF solved silently and you now decide on.
Secrets are not encryption. The Kubernetes docs are blunt: "Kubernetes Secrets are, by default, stored unencrypted in the API server's underlying data store (etcd)." Pair them with the External Secrets Operator plus AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, Scaleway Secret Manager or Vault, and enable encryption at rest on the cluster.
The managed-service question that cf create-service answered in one line. Pick a cloud managed database (RDS, Cloud SQL, Azure Database for PostgreSQL, Scaleway Managed Database), a Kubernetes operator (CloudNativePG, Zalando Postgres Operator), or a platform that provisions managed services for you. I will say it plainly: running your own stateful Postgres on Kubernetes is the worst effort-to-reward trade for most CF teams. You left a managed platform to get out of operational toil, so do not sign up to be a database SRE on day one.
Keep cf push. Lose the Cloud Foundry bill.
Qovery gives your developers git-push deploys, preview environments per pull request and self-service managed databases on your own AWS, GCP, Azure or Scaleway account - or your existing Kubernetes cluster. Start deploying in under 10 minutes.
How do routes, health checks and zero-downtime deploys work after Cloud Foundry?
Cloud Foundry's route, health check and rolling deploy become three explicitly configured Kubernetes pieces: an Ingress or Gateway API route with cert-manager TLS, Actuator-backed liveness, readiness and startup probes, and a Deployment with RollingUpdate, maxUnavailable: 0, a PodDisruptionBudget and a preStop hook. Miss the startup probe and a slow-starting JVM will crash-loop on day one.
Routing.cf map-route becomes an Ingress or a Gateway APIHTTPRoute. The Gateway API reached v1.0 GA on 2023-10-31 and is the forward-looking choice to standardize on, which matters more than it used to: the long-dominant ingress-nginx controller was retired by Kubernetes SIG Network in early 2026, so new clusters should look at a Gateway API implementation or a cloud-native controller like the AWS Load Balancer Controller or GKE Ingress rather than reaching for the old default. CF's platform-managed certificates become cert-manager (a CNCF graduated project) with Let's Encrypt, or a cloud certificate integration. CF's wildcard app domain becomes a per-environment wildcard DNS record you manage.
Probes. Enable management.endpoint.health.probes.enabled=true, point livenessProbe at /actuator/health/liveness and readinessProbe at /actuator/health/readiness (Actuator auto-enables these groups when it detects a Kubernetes environment), then add a startupProbe with a generous failureThreshold to cover JVM start time. This is the single most common failure I see after a CF migration, because a liveness probe with a short threshold starts killing the pod before Spring has finished starting.
Graceful shutdown. Boot's graceful shutdown drains in-flight requests, with spring.lifecycle.timeout-per-shutdown-phase defaulting to 30s. Match it with a terminationGracePeriodSeconds and a preStop sleep so the load balancer deregisters the pod before the JVM stops accepting connections. On Boot 4.x graceful shutdown is on by default; on 2.x and 3.x set server.shutdown=graceful.
Zero downtime. CF's rolling deploy maps to strategy: RollingUpdate with maxUnavailable: 0 and maxSurge: 1, plus a PodDisruptionBudget so node drains and cluster upgrades do not take the service down. CF gave you blue/green for free through route mapping. Kubernetes does not, so blue/green and canary need Argo Rollouts, Flagger or a service mesh.
Scaling.cf scale -i becomes replicas; CF App Autoscaler becomes an HPA on CPU or memory, or KEDA on queue depth, Kafka lag or HTTP RPS. One JVM caveat: CPU-based autoscaling is noisy during JIT warmup, and the HPA's default scale-down stabilization window is 300 seconds, so lean on a startup probe and conservative stabilization rather than letting the HPA thrash on warmup spikes.
Observability. Loggregator becomes stdout JSON logging shipped by Fluent Bit or Vector into Loki, OpenSearch or Datadog. CF metrics become Micrometer plus /actuator/prometheus. Add Micrometer Tracing with OpenTelemetry for the distributed traces CF never gave you in the first place.
What does a realistic Cloud Foundry to Kubernetes migration plan and timeline look like?
Run six phases and keep both platforms live in parallel until traffic is fully drained. In my experience a single Spring Boot service moves in days once a cluster exists, while a 20 to 50 app estate with shared service brokers realistically takes one to two quarters. The time goes into platform build-out, data migration and cutover rehearsal, not Java changes. Those numbers are my own experience, not a benchmark.
Phase
One service
20+ app portfolio
Main risk
Reversible
1. Inventory
1-2 days
2-3 weeks
Missing a hidden VCAP_* or local-disk dependency
Yes
2. Decouple config
1 day
2-4 weeks
A property binding that behaves differently
Yes
3. Package
1 day
2-3 weeks
OOMKills from untuned JVM memory
Yes
4. Build platform
shared
4-8 weeks
Under-sized or misconfigured cluster
Yes
5. Data and stateful
1-3 days
3-6 weeks
Data loss or drift during replication
Partly
6. Cutover
hours
ongoing
Latency or error regressions under real traffic
Yes, until decommission
Phase 1 - Inventory. List every app with its Java and Spring Boot version, bound services, routes, buildpack customizations, memory quota, and any VCAP_* or local-filesystem usage. Flag Boot 2.x apps immediately.
Phase 2 - Decouple config. Remove connectors, move to env-var-driven properties, redeploy on Cloud Foundry, and confirm nothing broke. This phase de-risks everything downstream and is fully reversible.
Phase 3 - Package. Adopt Paketo or Jib in CI, publish to your registry, then run the exact image locally with docker run and the same environment variables. If it runs locally, it runs in the cluster.
Phase 4 - Build the target platform. Cluster, ingress or Gateway controller, cert-manager, External Secrets, logging and metrics stacks, namespaces and RBAC mapped from CF orgs and spaces, and resource requests and limits derived from existing CF memory quotas rather than guessed.
Phase 5 - Data and stateful services. Provision managed databases, replicate with logical replication or dump/restore, plan a short write freeze or dual-write window, and rehearse the restore before cutover, not after.
Phase 6 - Cutover. Shift a percentage of traffic with weighted DNS or a shared load balancer, compare p95 latency, error rate and JVM GC behavior against the CF baseline, keep CF warm until it is drained, then decommission.
The pitfalls worth naming out loud, because I have seen each one in production: no startup probe on a slow JVM, memory limits without JVM tuning, writes to ephemeral local disk, hardcoded apps.internal service-to-service URLs, in-memory sessions after sticky routing disappears, and losing per-space quotas so one team's runaway app starves the whole cluster.
Should you move to raw Kubernetes, Korifi, a vendor platform, or an internal developer platform?
There are four credible destinations, and the right one depends almost entirely on how much platform engineering capacity you have, not on which technology is objectively best.
Raw Kubernetes plus Helm or Kustomize plus Argo CD. Maximum control, lowest license cost, highest ongoing ownership. If you already run a platform team, this is the correct answer and you do not need a product on top.
Korifi. The Cloud Foundry Foundation project reimplements the cf push experience on Kubernetes, backed by the CF V3 API running as Kubernetes custom resources. For a large estate of CF manifests and strong cf push muscle memory, it is a genuinely strong, lowest-friction path. Check its current release and supported API subset against your manifests before committing, since it does not implement every V3 endpoint yet.
Tanzu Platform or OpenShift. Tanzu Application Service and Tanzu Platform under Broadcom are the supported continuity path. They are also the documented reason many teams started evaluating alternatives: Broadcom completed the VMware acquisition on 2023-11-22 and then ended perpetual licensing in favor of subscription bundles. Red Hat OpenShift is the other enterprise destination, with its own source-to-image flow and Routes instead of Ingress.
An internal developer platform. Qovery, Northflank, Porter and self-hosted Coolify keep you in your own cloud account; Heroku, Render and Fly.io are options for teams willing to give that up.
My rule of thumb: under roughly 20 services with no dedicated platform team, adopting a developer platform on your own cluster beats rebuilding ingress, secrets, preview environments, RBAC and cluster upgrades from scratch. Above that, with a platform team that wants the control, build it on raw Kubernetes.
How does Qovery give back the Cloud Foundry developer experience on Kubernetes?
Qovery replaces the developer-facing half of Cloud Foundry (push-to-deploy, environments, self-service databases and role-based access) while everything runs inside your own AWS, GCP, Azure or Scaleway account, or on your existing Kubernetes cluster. The cloud contract, data residency and any committed-use discounts stay in your name.
Git-push deployments are the cf push equivalent. Developers push, Qovery builds from a Dockerfile or a buildpack-produced image and deploys to the cluster, and nobody hand-writes a manifest.
Preview environments per pull request are the clearest upgrade over Cloud Foundry, which never made per-PR environments cheap or self-service.
Environment auto-stop for non-production restores the cost discipline CF space quotas used to enforce, so idle namespaces stop burning cloud spend overnight.
Databases backed by managed cloud services are the practical replacement for CF service brokers, so provisioning a Postgres stays a self-service action instead of a ticket.
Per-environment RBAC maps onto the org and space model CF teams already think in, and managed cluster upgrades keep the Kubernetes version treadmill from becoming a new permanent job.
The boundary, stated honestly: you still choose your base image, JVM flags, probes and resource requests. Qovery hides Kubernetes from application developers, not from platform engineers, and it is a control plane rather than a hosting provider. It does not replace your cloud account, and it is not AWS-only. If your goal is to give developers back the cf push feeling without rebuilding the platform under it, that is exactly the gap it fills.
Frequently asked questions
Do I need to rewrite my Spring Boot code to run on Kubernetes instead of Cloud Foundry?
No. You delete Cloud Foundry glue (spring-cloud-connectors, VCAP_SERVICES parsing, the cloud profile) and bind to standard spring.datasource.* properties fed by environment variables. The business logic is untouched, and you can make these changes while still running on Cloud Foundry.
What replaces VCAP_SERVICES and spring-cloud-connectors on Kubernetes?
Plain Spring Boot properties sourced from Kubernetes Secrets and ConfigMaps. Inject them with envFrom/secretKeyRef, or project them as files and read them with spring.config.import=configtree:/etc/secrets/. For CF-like semantics, use the Service Binding Specification for Kubernetes or spring-cloud-bindings, which Paketo includes by default.
Can I keep using buildpacks instead of writing a Dockerfile for my Spring Boot app?
Yes. Run ./mvnw spring-boot:build-image to build a layered OCI image with Paketo Buildpacks, the same Cloud Native Buildpacks lineage Cloud Foundry adopted. No Dockerfile in the repo, and the Java memory calculator sizes the JVM from the container limit.
How do I map Cloud Foundry orgs, spaces and quotas to Kubernetes namespaces and RBAC?
A CF space becomes a Kubernetes namespace, CF org and space roles become RBAC Roles and RoleBindings, and CF space quotas become ResourceQuota and LimitRange objects. Derive the quota numbers from your existing CF memory quotas rather than guessing, so one team's app cannot starve the cluster.
How long does it take to migrate a Spring Boot application from Cloud Foundry to Kubernetes?
In my experience a single service moves in days once a cluster exists. A 20 to 50 app estate with shared service brokers realistically takes one to two quarters, because the time goes into building the platform, migrating data and rehearsing cutover, not changing Java.
Is Korifi or a Kubernetes PaaS like Qovery a better replacement for cf push?
Korifi is the better fit when you have hundreds of CF manifests and want to keep the literal cf push API, since it reimplements that API on Kubernetes. A platform like Qovery is the better fit when you want git-push deploys, preview environments and self-service databases in your own cloud account without running the control plane yourself.
Why do Spring Boot pods get OOMKilled on Kubernetes when they ran fine on Cloud Foundry?
Because a Kubernetes memory limit without JVM tuning gives the heap only about 25 percent of the pod's memory by default (MaxRAMPercentage), so the process is killed under load long before the limit looks full. Use the Paketo memory calculator, or set -XX:MaxRAMPercentage explicitly, and add a startup probe so a slow JVM start is not mistaken for a crash.
Romaric founded Qovery to make Kubernetes accessible to every engineering team. He writes about platform strategy, developer experience, and the future of cloud infrastructure.
Next step
Keep cf push. Lose the Cloud Foundry bill.
Qovery gives your developers git-push deploys, preview environments per pull request and self-service managed databases on your own AWS, GCP, Azure or Scaleway account - or your existing Kubernetes cluster. Start deploying in under 10 minutes.