Blue-Green GeoServer Deployments Behind an ALB
GeoServer is a stateful application pretending to be a stateless one. Its configuration — workspaces, stores, layers, styles, security rules — lives in a data directory on disk, and a rolling deployment that swaps containers underneath a shared data directory produces a window where two versions read and write the same catalogue with different schema expectations. The result is not a clean failure; it is a catalogue that mostly works with one layer definition missing, discovered days later by a user whose map lost a road network. Blue-green avoids that by never letting two versions touch one catalogue. This guide extends GeoServer Deployment Patterns within Geospatial Resource Provisioning.
The data directory is the whole problem
Everything difficult about deploying GeoServer follows from where the catalogue lives, and there are three arrangements with different consequences.
Shared filesystem, mounted read-write by every node. Convenient, and the arrangement that makes blue-green impossible in its pure form, because blue and green share the same catalogue by construction. Two versions writing it concurrently is exactly the corruption risk described above.
Baked into the image, read-only. The catalogue is built at image build time and shipped with the container. This makes GeoServer genuinely immutable and blue-green trivial: green carries its own catalogue and nothing is shared. The cost is that configuration changes require a rebuild and redeploy, so the administrative user interface becomes read-only in production — which is a discipline many teams want anyway, since a console-edited layer is unversioned infrastructure.
External catalogue in a database. A JDBC-backed catalogue moves configuration out of the filesystem, which solves the concurrency problem for reads but reintroduces it for schema: a version upgrade that changes the catalogue schema is a shared-database migration, and blue and green then disagree about the schema rather than about a directory.
For a version upgrade — the case blue-green is most needed for — the baked, read-only catalogue is the arrangement that actually delivers zero-downtime. The rest of this guide assumes it.
Prerequisites and environment assumptions
Terraform 1.6 or later with hashicorp/aws pinned at ~> 5.60. A GeoServer container image with the data directory baked in and digest-pinned. An application load balancer with two target groups, one per colour. The database and object stores GeoServer reads must be reachable from both colours — a green fleet that cannot reach PostGIS Cluster Provisioning will pass a container health check and fail every real request, which is the reason the health check below probes a layer rather than a port.
Know what the version upgrade does to your styles and layer definitions. A GeoServer major upgrade can change how a style is interpreted, and the whole point of blue-green is that you can compare rendered output between colours before switching — so plan to actually do that comparison rather than treating the switch as automatic.
Step-by-step deployment
-
Deploy green alongside blue, attached to its own target group. Green is not in the listener’s default action yet, so it receives no production traffic. It is, however, fully running and reachable through a test listener on a separate port.
-
Warm green before it sees traffic. GeoServer’s first request per layer is expensive — it opens stores, reads style definitions and populates caches — so a cold fleet promoted directly to production produces a latency spike that looks like a failed deployment. Request a representative tile from each significant layer against the test listener first.
-
Compare rendered output between colours. Fetch the same tile from blue and green and compare. Identical bytes mean the upgrade changed nothing visible; differences must be explained before switching, because a style interpreted differently by a new version is exactly what this step exists to catch.
-
Shift traffic by weight, not all at once. Move ten per cent, watch error rate and latency for a few minutes, then continue. A weighted listener rule makes this a configuration change rather than a deployment.
-
Hold blue for a full business cycle. Keep it running and reachable so a rollback is a weight change taking seconds. Destroying blue immediately after the switch converts a two-minute rollback into a redeployment.
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.60" }
}
}
variable "green_weight" {
type = number
default = 0
description = "0 to 100. Shift gradually; this variable IS the deployment."
}
resource "aws_lb_target_group" "blue" {
name = "geoserver-blue"
port = 8080
protocol = "HTTP"
vpc_id = var.vpc_id
target_type = "ip"
health_check {
# Probe a real WMS GetCapabilities, not the container port. A GeoServer
# that cannot reach PostGIS still binds 8080 and still answers TCP, so a
# port check keeps a functionally dead node in rotation indefinitely.
path = "/geoserver/wms?service=WMS&version=1.3.0&request=GetCapabilities"
matcher = "200"
interval = 30
healthy_threshold = 2
unhealthy_threshold = 3
timeout = 10
}
}
resource "aws_lb_target_group" "green" {
name = "geoserver-green"
port = 8080
protocol = "HTTP"
vpc_id = var.vpc_id
target_type = "ip"
health_check {
path = "/geoserver/wms?service=WMS&version=1.3.0&request=GetCapabilities"
matcher = "200"
interval = 30
healthy_threshold = 2
unhealthy_threshold = 3
timeout = 10
}
}
resource "aws_lb_listener" "public" {
load_balancer_arn = aws_lb.geoserver.arn
port = 443
protocol = "HTTPS"
certificate_arn = var.certificate_arn
default_action {
type = "forward"
forward {
target_group {
arn = aws_lb_target_group.blue.arn
weight = 100 - var.green_weight
}
target_group {
arn = aws_lb_target_group.green.arn
weight = var.green_weight
}
# Sticky sessions matter for GeoServer's administrative interface and for
# any client relying on a session; without them a mid-shift request can
# land on the other colour and appear to lose state.
stickiness {
enabled = true
duration = 300
}
}
}
}
# A separate listener on a test port so green can be warmed and compared
# before it is in the public path at all.
resource "aws_lb_listener" "green_test" {
load_balancer_arn = aws_lb.geoserver.arn
port = 8443
protocol = "HTTPS"
certificate_arn = var.certificate_arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.green.arn
}
}
variable "vpc_id" { type = string }
variable "certificate_arn" { type = string }
# Step 2 and 3: warm green, then compare rendered output against blue.
LAYERS="topp:states osm:roads gis:parcels"
BBOX="-122.5,37.7,-122.3,37.9"
for layer in $LAYERS; do
# Warm: the first request per layer opens stores and reads styles.
curl -sf -o /dev/null "https://maps.example.org:8443/geoserver/wms\
?service=WMS&version=1.3.0&request=GetMap&layers=${layer}\
&bbox=${BBOX}&width=512&height=512&crs=EPSG:4326&format=image/png"
# Compare: identical bytes mean the upgrade changed nothing visible.
curl -sf -o "/tmp/blue-${layer//:/_}.png" "https://maps.example.org/geoserver/wms?...&layers=${layer}"
curl -sf -o "/tmp/green-${layer//:/_}.png" "https://maps.example.org:8443/geoserver/wms?...&layers=${layer}"
if ! cmp -s "/tmp/blue-${layer//:/_}.png" "/tmp/green-${layer//:/_}.png"; then
echo "RENDER DIFFERS for ${layer} — explain before shifting traffic"
fi
done
Verification
At each weight, check the same four things: error rate at the load balancer by target group, p99 latency by target group, the health of every target in green, and a rendered tile fetched through the public endpoint. Comparing per target group rather than in aggregate is what makes a green-only problem visible while ninety per cent of traffic is still on blue.
After reaching full weight, verify the catalogue is complete on green rather than assuming it: GetCapabilities should list the same layer count as blue, and a spot check of styles on the most complex layers should render as expected. A missing layer at this point is recoverable in seconds; discovered a week later, after blue has been destroyed, it is a rebuild.
Preventing recurrence
- Keep the production catalogue read-only. Every console edit is a change that exists in one colour and not the other, and blue-green cannot protect a catalogue that is being edited underneath it.
- Make the render comparison a pipeline stage. An automated byte comparison across a fixed layer and bounding-box set turns “we should check the styles” into a gate, using the probe approach from Testing and Validation for Spatial IaC.
- Health-check a real service response. A port check keeps a GeoServer that has lost its database connection in rotation, answering every request with an error.
- Automate the hold-and-destroy schedule. Blue should be destroyed on a timer after a successful cycle, not when someone remembers, and not immediately.
Frequently Asked Questions
Can I do blue-green with a shared data directory?
Not meaningfully. The shared catalogue is exactly what the two colours must not share, so the deployment gives you two fleets and none of the isolation. Bake the catalogue into the image, or accept that you are doing a rolling deployment with its concurrency risk.
How long should the traffic shift take?
Long enough to observe a full traffic pattern at each weight, which for a map platform means at least several minutes per step and ideally spanning a peak. Shifting in under a minute gives the metrics no time to move and reduces the exercise to a slower way of switching instantly.
Do I need sticky sessions?
For the administrative interface and any session-dependent client, yes, or a mid-shift request lands on the other colour and appears to lose state. For pure stateless tile and WMS traffic they are unnecessary, and if the platform serves only that, leaving them off distributes load more evenly.
What if green renders differently and the difference is correct?
Then it is a deliberate visual change and should be released as one — announced, and ideally not on the same day as a version upgrade. The value of the comparison is that it forces the difference to be explained rather than discovered by a user.
Related
- GeoServer Deployment Patterns — the parent topic on running GeoServer as managed infrastructure
- Pulumi Programmatic Resource Mapping for GeoServer — expressing the catalogue as code rather than as console state
- Auto-Scaling EC2 Instances for WMS Endpoints — sizing each colour’s fleet
- Vector Tile Service Provisioning — the geometry-serving tier that usually sits beside this one