Purpose
This procedure describes how to migrate Cloud Foundry applications from stack cflinuxfs4 to stack cflinuxfs5 using a patch + rolling restage approach — without requiring the original application source or manifest to be re-pushed.
Prerequisites
- CF CLI v8.1.0 or later — required for
--strategy rolling.
cf version
- jq installed — used to parse and format CF API output.
jq --version
- Proxy environment variables — set these if your network requires an outbound proxy to reach the CF API endpoint.
HTTP_PROXY=http://<proxy-host>:<proxy-port>
https_proxy=http://<proxy-host>:<proxy-port>
http_proxy=http://<proxy-host>:<proxy-port>
HTTPS_PROXY=http://<proxy-host>:<proxy-port>
- Sufficient permissions (Space Developer or higher) in every targeted org/space.
- A current inventory of affected applications and their stacks — see Section 2.
Inventory affected applications
List all applications across orgs/spaces along with their current stack and buildpack, so you know exactly what needs migrating.
cf curl "/v3/apps?per_page=5000&include=space.organization" | jq '
(.included.spaces | INDEX(.guid)) as $spaces |
(.included.organizations | INDEX(.guid)) as $orgs |
[ .resources[] | {
app: .name,
org: $orgs[$spaces[.relationships.space.data.guid].relationships.organization.data.guid].name,
space: $spaces[.relationships.space.data.guid].name,
lifecycle
} ]'
Organize the output into a table, sorted by stack and space, so migration targets are clear:
| Stack | Space | App | Buildpack |
|---|---|---|---|
| cflinuxfs4 | env-1 | app-a | buildpack-x |
| cflinuxfs4 | env-1 | app-b | buildpack-y |
| cflinuxfs4 | env-2 | app-a | buildpack-x |
| cflinuxfs5 | env-3 | app-a | (auto-detected) |
Inspect any individual application in more detail before migrating it:
cf app <app-name>
Watch the instance countPay attention to the instances field in the output — this affects whether the migration can be truly zero-downtime. More on this in Section 4.
Check instance count for one app
cf app <app-name> | grep instances
Check instance counts for every app in the current space
cf curl "/v3/apps?per_page=5000" | jq -r '.resources[].guid' | while read guid; do
name=$(cf curl /v3/apps/$guid | jq -r '.name')
instances=$(cf curl /v3/apps/$guid/processes | jq '[.resources[] | select(.type=="web")][0].instances')
echo "$name: $instances"
done
Migration steps (per application)
Patch the desired stack
This updates the application's desired-state record only. It does not rebuild or redeploy anything by itself.
cf curl /v3/apps/$(cf app <app-name> --guid) -X PATCH \
-d '{"lifecycle":{"type":"buildpack","data":{"stack":"cflinuxfs5"}}}'
Restage with a rolling strategy
This is the step that actually rebuilds the droplet against the new stack and performs the deployment.
cf restage <app-name> --strategy rolling
Rolling restage starts new instance(s) on the new stack, health-checks them, and only then removes the old instance(s) — reducing (but not always eliminating) downtime.
Important: instance count affects downtime
--strategy rolling only reduces downtime risk with 2+ instances.With a single instance (1/1), there is no second instance to serve traffic while the new one starts, so a brief gap is still likely.
Recommended for single-instance apps
cf scale <app-name> -i 2
cf restage <app-name> --strategy rolling
cf scale <app-name> -i 1 # scale back down if 1 is the normal desired state
Risks of temporary scale-up
- Quota — doubles memory consumption while at 2 instances; confirm org/subaccount has headroom before scaling, or the scale may fail.
- Shared state — if the app holds local/in-memory session state, briefly running 2 instances can cause inconsistent behavior for users mid-request (not downtime, but a data-consistency risk).
- Scaling up/down itself does not cause downtime — the existing instance keeps serving throughout.
Checking quota headroom before scalingIf you want to check the headroom before scaling up, you can utilize this script — it generates a human-readable table of memory usage.
Batch migration script (per org/space)
cf target -o "<org-name>" -s "<space-name>"
for app in app-a app-b app-c; do
guid=$(cf app "$app" --guid)
echo "Patching $app ($guid) to cflinuxfs5..."
cf curl /v3/apps/$guid -X PATCH \
-d '{"lifecycle":{"type":"buildpack","data":{"stack":"cflinuxfs5"}}}'
echo "Restaging $app with rolling strategy..."
cf restage "$app" --strategy rolling
done
Repeat per org/space, migrating lower environments first (e.g. dev → qa → prod) and validating at each stage before proceeding.
Validation checklist (per application)
cf app <app-name>shows the new stack.- Droplet was actually rebuilt (staging timestamp / droplet GUID changed — not just lifecycle metadata).
- Buildpack version used after migration matches expectations (newer stacks may auto-detect newer buildpack versions — check for runtime version drift).
cf logs <app-name> --recentshows no staging or startup errors.- Application routes respond as expected (health check / smoke test).
- Instance count restored to its normal desired state, if temporarily scaled up.
Rollback
If an application fails to start cleanly on the new stack:
cf curl /v3/apps/$(cf app <app-name> --guid) -X PATCH \
-d '{"lifecycle":{"type":"buildpack","data":{"stack":"cflinuxfs4"}}}'
cf restage <app-name> --strategy rolling
Notes and caveats
- Task-like or one-shot applications (e.g. database migration/deployer jobs) may not benefit from
--strategy rollingthe same way long-running services do — evaluate case by case whether rolling restage is appropriate, or whether a simple restage/redeploy suffices. - Always migrate and validate lower environments before touching production.
- Keep a second terminal open on
cf logs <app-name> --recentduring each restage to catch failures early.
Appendix A — Memory usage report script
This helper script displays a quick overview of an org's memory quota (total / used / free) alongside a per-app memory usage breakdown for the currently targeted space. Useful for confirming quota headroom before scaling an app up — see Section 4.
No modifications requiredSimply target the org/space you want to check, then run the script as-is.
cf target -o "<org-name>" -s "<space-name>"
#!/usr/bin/env bash
# Displays org memory quota (total / used / free) plus a per-app memory usage
# breakdown for the currently targeted org/space.
#
# Requires: cf CLI (logged in + targeted), jq
# Usage: ./memory_usage_table.sh
# Auto-detects a 'cf' binary on PATH, or falls back to ./cf in the
# current directory if 'cf' is not found on PATH.
set -euo pipefail
if command -v cf >/dev/null 2>&1; then
CF="cf"
elif [[ -x "./cf" ]]; then
CF="./cf"
else
echo "Error: 'cf' not found on PATH and no executable ./cf in current directory." >&2
exit 1
fi
# ---- Org-level quota summary -------------------------------------------
org_name=$($CF target | sed -n 's/^org:[[:space:]]*//p')
space_name=$($CF target | sed -n 's/^space:[[:space:]]*//p')
org_guid=$($CF org "$org_name" --guid)
quota_name=$($CF org "$org_name" | sed -n 's/^quota:[[:space:]]*//p')
total_mb=$($CF quota "$quota_name" | sed -n 's/^total memory:[[:space:]]*//p')
used_mb=$($CF curl "/v3/organizations/${org_guid}/usage_summary" | jq -r '.usage_summary.memory_in_mb')
# normalize total_mb ("200G" / "20480M" / plain number) to MB
if [[ "$total_mb" == *G ]]; then
total_mb=$(( ${total_mb%G} * 1024 ))
elif [[ "$total_mb" == *M ]]; then
total_mb=${total_mb%M}
fi
free_mb=$(( total_mb - used_mb ))
echo "=================================================================="
printf "ORG: %s\n" "$org_name"
printf "SPACE: %s\n" "$space_name"
echo "=================================================================="
echo ""
printf "%-20s %-15s\n" "METRIC" "MEMORY (MB)"
printf "%-20s %-15s\n" "--------------------" "---------------"
printf "%-20s %-15s\n" "Total Quota" "${total_mb}MB"
printf "%-20s %-15s\n" "Current Usage" "${used_mb}MB"
printf "%-20s %-15s\n" "Free" "${free_mb}MB"
echo ""
# ---- Per-app memory breakdown (current space) --------------------------
echo "Per-app memory usage in space '$space_name':"
echo ""
printf "%-30s %-12s %-15s %-15s\n" "APP" "INSTANCES" "MEM/INSTANCE" "TOTAL MEM"
printf "%-30s %-12s %-15s %-15s\n" "------------------------------" "------------" "---------------" "---------------"
space_guid=$($CF curl "/v3/spaces?names=${space_name}&organization_guids=${org_guid}" | jq -r '.resources[0].guid // empty')
app_total_mb=0
if [[ -z "$space_guid" ]]; then
printf "%-30s\n" "(could not resolve space GUID)"
else
apps=$($CF curl "/v3/apps?space_guids=${space_guid}&per_page=5000" | jq -r '.resources[]? | "\(.guid) \(.name)"')
if [[ -z "$apps" ]]; then
printf "%-30s\n" "(no apps found in this space)"
else
while read -r guid name; do
[[ -z "$guid" ]] && continue
proc=$($CF curl "/v3/apps/${guid}/processes" | jq '[.resources[]? | select(.type=="web")][0] // {}')
instances=$(echo "$proc" | jq -r '.instances // 0')
mem=$(echo "$proc" | jq -r '.memory_in_mb // 0')
this_total=$(( instances * mem ))
app_total_mb=$(( app_total_mb + this_total ))
printf "%-30s %-12s %-15s %-15s\n" "$name" "$instances" "${mem}MB" "${this_total}MB"
done <<< "$apps"
fi
fi
printf "%-30s %-12s %-15s %-15s\n" "------------------------------" "------------" "---------------" "---------------"
printf "%-30s %-12s %-15s %-15s\n" "TOTAL (this space)" "" "" "${app_total_mb}MB"
echo ""
echo "Note: 'Current Usage' above is org-wide (all spaces); the per-app table"
echo "only covers apps in the currently targeted space."
Usage
chmod +x memory_usage_table.sh
cf target -o "<org-name>" -s "<space-name>"
./memory_usage_table.sh
Sample output
==================================================================
ORG: <org-name>
SPACE: <space-name>
==================================================================
METRIC MEMORY (MB)
-------------------- ---------------
Total Quota 204800MB
Current Usage 1280MB
Free 203520MB
Per-app memory usage in space '<space-name>':
APP INSTANCES MEM/INSTANCE TOTAL MEM
------------------------------ ------------ --------------- ---------------
app-a 1 256MB 256MB
app-b 1 256MB 256MB
app-c 1 512MB 512MB
------------------------------ ------------ --------------- ---------------
TOTAL (this space) 1024MB
Note: 'Current Usage' above is org-wide (all spaces); the per-app table
only covers apps in the currently targeted space.