Developer Guides

How to Migrate Legacy Monolith Web Apps to Microservices with Docker and Kubernetes

Migrating a legacy monolithic web application to microservices with Docker and Kubernetes is best done incrementally. By containerizing individual components and deploying them within Kubernetes, you can gradually shift functionality while keeping the monolith active.

This coexistence strategy enables rollback and safer migration without downtime. You’ll need a Kubernetes cluster, Docker setup, and container registry access to begin orchestrating your new microservices alongside the old system.

Cats interacting with tech equipment (monitors, servers, coding interfaces) in minimalist wireframe environments.: How to Migrate Legacy Monolith Web Apps to Microservices with Docker and Kubernetes

Quick answer

Start by confirming your environment: ensure a Kubernetes cluster is running and accessible via kubectl. Validate that Docker is installed and ready to build images, and verify you can authenticate with your container registry to push images.

Next, containerize your monolith’s components by building Docker images with appropriate tags, then push these images to your registry. Deploy them to Kubernetes by applying Deployment manifests specifying replicas, labels, and ports.

Create Kubernetes Services to expose microservices internally using stable endpoints for communication. Implement a routing layer or proxy that directs traffic incrementally to microservices while routing remaining requests to the monolith, enabling smooth coexistence.

Finally, verify the deployments and routing correctness by inspecting Deployment and Pod status and testing request flows.

Step-by-step guide

Prerequisites and Starting Environment

Before migrating your legacy monolithic web app to microservices with Docker and Kubernetes, set up your environment carefully to avoid surprises.

First, ensure you have a running Kubernetes cluster accessible through your local machine. Check this by running kubectl version — you should see output confirming communication with the cluster.

Next, prepare your Docker environment. Validate Docker is installed and running so you can build container images from your app's components.

Finally, confirm you can authenticate with your chosen container registry (Docker Hub or another), because you'll need to push your built images there before deploying them in Kubernetes.

This preparation ensures you have the core infrastructure ready for building images, deploying containers, and orchestrating them smoothly as you gradually break down your monolith.

Sources: Build, tag, and publish an image; Run a Stateless Application Using a Deployment; Connect a Frontend to a Backend Using Services.

Build and Publish Docker Images for Selected Monolith Components

The first practical step in migrating your monolithic app is to containerize its individual components by building Docker images. Start in the directory of the chosen component where your Dockerfile is located.

Run the command docker build -t <namespace>/<repository>:<tag> . to create an image, replacing placeholders with your registry username, repository name, and version tag. This tagging follows the naming conventions of your container registry and helps manage image versions.

Once built, authenticate with your container registry using docker login to ensure you have permission to push. Then, publish the image using docker push <namespace>/<repository>:<tag>.

Verifying the upload is crucial: check the registry's Tags section to confirm your image is present.

This process prepares your microservice components as portable, versioned containers ready for deployment in Kubernetes, paving the way to incremental migration without disrupting the entire application.

Sources: Build, tag, and publish an image.

Deploy Microservices as Kubernetes Deployments

After building and publishing Docker images for your application components, the next step is to deploy these microservices as Kubernetes Deployments. Start by creating a Deployment manifest YAML file that specifies the container image you pushed to your registry.

Include important fields such as replicas to define the number of pod instances for scalability, labels to organize your pods, and containerPort matching your service's exposed port. Use the command kubectl apply -f <deployment-file>.yaml to deploy your microservice to the cluster.

This step ensures your microservices run reliably and can be managed and scaled easily within Kubernetes. To confirm the deployment worked, run kubectl get deployments to see Deployment status, and kubectl get pods -l <label-selector> to list the pods created.

If pods are running, your microservice is active. In case of issues, kubectl describe deployment <name> shows detailed status and events, helping identify any problems with scheduling or startup.

This approach aligns with the Kubernetes recommended way to run stateless applications via Deployments, facilitating incremental migration by letting each microservice be managed independently within the cluster environment.

Sources: Run a Stateless Application Using a Deployment.

Create Kubernetes Services to Expose Microservices Internally

After deploying your microservices as Kubernetes Deployments, the next crucial step is to create Kubernetes Services that expose these microservices internally within the cluster. Services provide a stable network endpoint and abstract away the dynamic pod IP addresses, enabling reliable in-cluster communication.

To create a Service, write a manifest YAML file that specifies the Service type as ClusterIP (the default), which exposes the microservice only within the Kubernetes cluster. Set the selector to match the pod labels defined in your Deployment and specify the port the microservice listens on and the targetPort Kubernetes uses to forward traffic.

Apply this manifest using kubectl apply -f <service-file>.yaml. This creates the Service resource, allowing other components or microservices in the cluster to connect consistently to your newly created microservice through its stable DNS name or ClusterIP.

You can verify the Service by running kubectl get services to see its status and confirm the assigned cluster IP address. For deeper inspection, kubectl describe service <service-name> shows detailed configuration.

This approach avoids coupling directly to ephemeral Pod IPs and fits well with incremental migration — your legacy components or other microservices can now route requests to the new service endpoint safely. If your cluster environment doesn’t support certain external access methods, consider alternatives like NodePort Services according to your architecture.

Following these steps gives you a robust way to expose microservices internally and manage communication during your migration from monolith to microservices.

Source: Official Kubernetes documentation on connecting frontend and backend using Services (2026).

Sources: Connect a Frontend to a Backend Using Services.

Implement Incremental Routing and Coexistence with the Legacy Monolith

To implement incremental routing while your legacy monolith and new microservices coexist, set up a routing layer or proxy that can intercept requests and direct only selected paths or features to the corresponding microservices. This approach aligns with the strangler fig pattern, which migrates components gradually without downtime or an all-at-once rewrite.

Start by deploying a reverse proxy or API gateway within your Kubernetes cluster. Configure it to forward requests matching the migrated functionality to the new microservices’ Kubernetes Service endpoints created earlier, while routing all other requests to the monolith.

This allows you to maintain full application availability with a clear rollback path—if issues arise, you can revert traffic back to the monolith simply by adjusting proxy rules.

Ensure your proxy supports fine-grained routing rules and health checks to avoid becoming a single failure point or bottleneck. Monitor the proxy’s performance and error rates carefully throughout migration to detect issues early.

As traffic shifts incrementally to the microservices, retire the corresponding monolith functionality once confident in the new service’s stability. This layered coexistence balances migration risk and operational continuity effectively.

If your system cannot intercept requests cleanly or has minimal components to migrate, evaluate whether this coexistence complexity is justified, since the proxy itself may introduce overhead or failure risks.

You can verify routing by testing that requests meant for new features hit the microservices directly and others fall back to the monolith. Adjust your proxy configuration iteratively to ensure seamless user experience.

Sources: Strangler fig pattern.

Verify Deployment and Functionality of Migrated Microservices

Start by verifying that your Kubernetes Deployments are running as intended. Use kubectl get deployments to confirm the desired number of replicas is available.

Then, list the Pods with kubectl get pods -l <label-selector> matching your Deployment labels; all should be in the "Running" state.

Check the Services exposing your microservices internally via kubectl get services. Verify that each Service has a stable ClusterIP and that its selector correctly matches the target Pods.

Validate routing by sending test requests through your ingress or proxy layer. Confirm that traffic for migrated functions reaches the new microservices and that fallback to the legacy monolith occurs correctly for other requests.

If Pods fail to start or are pending, use kubectl describe pod <pod-name> to inspect event logs and error messages. This helps identify resource or scheduling problems.

Similarly, kubectl describe deployment <deployment-name> provides deployment-level health and rollout status.

This verification process ensures your incremental migration is functioning, with new microservices properly deployed, exposed, and integrated for fail-safe coexistence with the legacy application.

Sources: Strangler fig pattern; Run a Stateless Application Using a Deployment; Connect a Frontend to a Backend Using Services.

Troubleshooting Deployment Issues and Routing Errors

Troubleshooting deployment and routing issues in your Kubernetes migration starts with inspecting the status of your Pods. Use the command kubectl describe pod <pod-name> to view detailed information and recent events for problematic Pods.

This output will highlight reasons such as resource scheduling failures—for example, insufficient CPU or memory—that prevent Pods from running.

To catch broader cluster events including scheduling problems, run kubectl get events --namespace=<namespace>. This helps identify runtime errors or conflicts affecting your microservice.

If Pods remain stuck in Pending or CrashLoopBackOff states, verify that your cluster has enough capacity and that resource requests in your Deployment are appropriate.

For routing issues, investigate your proxy or API gateway configuration: ensure rules correctly split traffic between the legacy monolith and new microservices per the strangler fig pattern. Monitor the proxy to avoid it becoming a bottleneck or point of failure.

By systematically checking Kubernetes events and proxy behavior, you can detect and fix deployment or routing bottlenecks to maintain smooth coexistence and incremental migration progress.

Sources: Strangler fig pattern; Debug Running Pods.

Next steps

Migrating your monolith incrementally to microservices with Docker and Kubernetes works well if you prepare your environment correctly and follow the steps to build, deploy, and expose containerized components. Implementing a routing layer for coexistence is critical to maintain availability and rollback paths.

Verify that all Deployments and Services run as expected, and monitor your routing proxy to avoid bottlenecks or failures. With careful execution, you can balance the benefits of microservices against orchestration overhead and keep your application stable during transition.