Working with modern multi-stage Dockerfiles on legacy server environments can feel like fitting a square peg into a round hole. While updating the container infrastructure for flexiride-backend—a monorepo running 15 microservices—I ran into a wall with a legacy Docker Engine environment (v1.29.2).
Here is how I adapted our multi-stage build pipeline across 9 active microservices without requiring BuildKit, root privileges, or breaking our build architecture.
The Challenge: Modern Multi-Stage vs. Legacy Daemon
Our goal was straightforward: standardize Dockerfile.base across our active microservices and gateways (auth, user, driver, booking, matching, payment, notification, web-bff, and mobile-bff) to produce consistent :jvm runtime images.
However, legacy Docker v1.29.2 presented three hard constraints:
-
No BuildKit Support: Modern caching mechanisms like
--mount=type=cachefailed instantly. - Sequential Target Evaluation: Without BuildKit's smart DAG evaluation, legacy Docker couldn't skip late-stage GraalVM native image compilation steps, even when we only requested the intermediate JVM runtime target.
- Restricted Environment: No root privileges were available to upgrade the underlying local engine or daemon.
The Workarounds
1. Reordering Dockerfile.base Targets
Because legacy Docker processes build stages sequentially, putting heavy or optional downstream stages first forces the builder to run them regardless of target flags.
By reordering Dockerfile.base to prioritize JVM runtime stages before GraalVM steps, the build pipeline completes the JVM image cleanly before hitting unsupported toolchains.
2. Explicit Stage Targets in Docker Compose
To force legacy Docker to stop cleanly at the JVM stage, I declared explicit build targets across docker-compose.yml:
services:
auth:
build:
context: .
dockerfile: Dockerfile.base
target: "false" # Explicitly targets JVM runtime stage on legacy engines
image: flexirides/auth:jvm
This stopped legacy Docker from executing past the JVM stage and kept image naming completely standardized across services (flexirides/<service-name>:jvm).
Verification & Results
After updating the build definitions, all 9 microservices compiled, tagged, and ran successfully.
1. Tagged Image Consistency
We verified that all 9 images compiled completely without errors and followed standardized naming conventions (flexirides/<service-name>:jvm):

2. Service Bootstrapping
Spun up the auth microservice using docker-compose up -d auth and monitored its transition to an Up (healthy) state:

3. Spring Boot Actuator Health Check
Finally, we queried the service's health endpoint (GET http://localhost:8086/actuator/health) to confirm database connectivity and application readiness:

HTTP/1.1 200 OK
Content-Type: application/json
{
"status": "UP"
}
Key Takeaways
- Stage ordering matters on older Docker daemons that lack BuildKit's dynamic target skipping.
-
Explicit
targetdeclarations indocker-compose.ymlprevent legacy engines from running unnecessary build stages. -
Always validate microservice readiness with runtime health checks (like Spring Actuator) rather than assuming a successful
docker buildmeans a working container.
Have you had to support legacy Docker engines in your production environments? How did you handle multi-stage builds? Let's discuss below!
Top comments (0)