DEV Community

M TOQEER ZIA
M TOQEER ZIA

Posted on

Building a Microservices App with Kubernetes: What I Learned the Hard Way

From Docker to Minikube, Ingress, Services, and Skaffold in one real project

Introduction

I did not learn Kubernetes by reading theory alone. I learned it by building a real microservices app, breaking things, fixing them, and slowly seeing how all the parts fit together.

The project was a simple blogging app with posts and comments, but the architecture behind it was the real lesson. Instead of putting everything into one big backend, I split the app into small services: Posts, Comments, Moderation, Query, and an Event Bus. The frontend was a React client. Each service had one clear job. That choice forced me to understand containers, service discovery, deployments, networking, ingress, and local Kubernetes with Minikube.

This is my learning journey written in simple English. I am sharing what I built, why I built it this way, what confused me, and what finally made the whole picture click.


Table of Contents

  1. Introduction to the Project
  2. Understanding Containers
  3. Kubernetes Basics
  4. Kubernetes Architecture
  5. Deployments
  6. Services
  7. Load Balancing
  8. Communication Between Microservices
  9. Ingress Controller
  10. Host File Configuration
  11. Minikube
  12. Minikube IP
  13. Docker Inside Minikube
  14. Skaffold
  15. Important Commands Explained
  16. Common Problems I Faced
  17. Pros and Cons
  18. Lessons I Learned
  19. Best Practices
  20. Conclusion
  21. What’s Next?
  22. SEO Details

1. Introduction to the Project

What is this project?

I built a small blogging app where users can create posts and add comments. The interesting part is not the app idea itself. The interesting part is how the app behaves behind the scenes.

The system is split into multiple services:

  • Posts Service creates posts
  • Comments Service creates comments
  • Moderation Service checks comments and approves or rejects them
  • Query Service keeps a read-optimized view for the UI
  • Event Bus forwards events between services
  • React Client shows the user interface

This project helped me understand event-driven microservices, CQRS, and eventual consistency. In simple words, one service does not try to do everything. Instead, each service does one thing well and talks to others through events.

Why microservices?

I asked myself a very practical question: why not just build one backend?

The answer became clearer as the project grew. A monolithic app is easier at the beginning, but it can become heavy later. Every change affects the same codebase and one bug can slow down the whole system.

Microservices solved a few real problems for me:

  • Each service could be built and understood separately
  • Each service could be deployed separately
  • One slow service did not need to block the others
  • It was easier to model real business flow as events
  • I could practice service-to-service communication the way larger systems do it

Why not a monolithic application?

I did think about a monolith first. For a small demo, that would have been simpler. But my goal was learning, not just shipping the shortest possible code.

If I had used one monolith, I would have missed the hard but useful parts:

  • How services discover each other
  • How Kubernetes manages containers
  • Why ingress matters
  • Why internal services use ClusterIP
  • How async events keep the system flexible

So I chose the harder path because it taught me more.

What problems microservices solve

Microservices help when different parts of an application need to move at different speeds.

For example, in my project:

  • Posts are created quickly
  • Comments may need moderation
  • The UI reads from a separate query model
  • The system should not freeze just because one service is busy

That is the kind of problem microservices handle well.

Architecture overview

Here is the high-level picture I worked with:

React Client
    |
    v
Ingress
    |
    +------------------------+
    |                        |
    v                        v
Posts Service           Query Service
    |                        ^
    v                        |
Event Bus --------------> Moderation Service
    |
    v
Comments Service
Enter fullscreen mode Exit fullscreen mode

The flow is event-based. A request starts in the client, goes to a service, and then that service emits an event to the event bus. The event bus shares that event with the other services that care about it.

That separation was one of the most useful lessons in the whole journey.


2. Understanding Containers

What is Docker?

Docker was my entry point into this whole path. Before Docker, I used to think, “Why does an app work on my machine but fail somewhere else?” Docker made that question smaller.

Docker lets me package an application with everything it needs to run: code, runtime, dependencies, and configuration. That package is called an image. When the image runs, it becomes a container.

Why containers exist

Containers exist because software environments are messy.

One machine may have Node.js 16. Another may have Node.js 20. One machine may have missing packages. Another may have a different operating system. Containers reduce that problem by giving the app a predictable environment.

The easiest way I understood it was this:

  • An image is the blueprint
  • A container is the running house built from that blueprint

Images vs containers

This confused me at first, so I kept it simple.

  • Image: the saved template
  • Container: the live running instance

If I build the same image ten times, I get ten containers. They all come from the same starting point, but each container runs separately.

Why Kubernetes needs Docker

Strictly speaking, Kubernetes does not require Docker specifically anymore, but in my learning path Docker was the tool that built the container images Kubernetes used.

Kubernetes does not care how the image was built. It cares that the image exists and can be pulled by a node. Docker helped me create those images in a familiar way.

How containers communicate

In the project, containers communicate over the network. Each service listens on a port, and other services call that port using HTTP.

For example:

  • Posts Service listens on port 8000
  • Comments Service listens on port 8001
  • Query Service listens on port 8002
  • Moderation Service listens on port 8003
  • Event Bus listens on port 5000

One service sends a request to another service using its network address and port. That is simple in theory, but in Kubernetes it becomes easier when we use Services and DNS names instead of raw IP addresses.

Simple example

If I want the Posts Service to send an event, it posts to the Event Bus endpoint:

Posts Service -> http://event-bus-srv:5000/events
Enter fullscreen mode Exit fullscreen mode

That internal address is much better than hardcoding a Pod IP, because Pods can change.


3. Kubernetes Basics

What is Kubernetes?

Kubernetes is a system that helps me run containers across one or more machines. It handles placement, restarts, scaling, and networking.

The easiest way I can describe it is this: Docker runs containers, but Kubernetes organizes them.

Why Kubernetes exists

I used to think Kubernetes was just a fancy way to run Docker. It is not.

It exists because running one container is easy, but running many containers reliably is hard. Once I had more than one service, I started asking real orchestration questions:

  • What if a container dies?
  • What if traffic increases?
  • Which machine should run which container?
  • How do services find each other?
  • How do I update a service without breaking everything?

Kubernetes helps answer those questions.

What problems Kubernetes solves

Kubernetes solves a few practical problems:

  • It restarts failed containers
  • It spreads containers across nodes
  • It scales applications up and down
  • It gives containers stable network identities through Services
  • It supports rolling updates
  • It makes local and production setups more similar

Cluster

A cluster is the whole Kubernetes environment. It is the group of machines that work together.

I like to think of it like a small company. The company is the cluster. Inside it, there are different employees doing different jobs.

Node

A node is one machine inside the cluster. It is where Pods run.

If the cluster is the company, the node is one office building.

Master Node or Control Plane

The master node, also called the control plane, is the brain of Kubernetes.

It decides:

  • Which node gets a Pod
  • What happens when a Pod dies
  • When replicas should be created or removed
  • How the cluster stays in the desired state

I do not normally run my app there. I let it manage the app.

Worker Node

Worker nodes are the machines that actually run my Pods and containers.

If the control plane is the manager, worker nodes are the people doing the actual work.

Pod

A Pod is the smallest deployable unit in Kubernetes.

This was one of my biggest mental shifts. Kubernetes does not usually run a raw container directly. It runs a Pod, and the Pod contains one or more containers.

I usually used one container per Pod in this project.

Container inside a Pod

A container inside a Pod is the actual process running my app.

The Pod gives it networking and shared storage if needed. The container is still the thing running Node.js or React.

Replica

A replica is another copy of the same Pod.

If one Pod is not enough to handle traffic, Kubernetes can run two, three, or more copies. That helped me understand scaling in a very simple way.

Namespace

A namespace is a way to separate resources inside the same cluster.

I think of it like separate rooms inside the same building. Everything is still inside the cluster, but the rooms help keep things organized.

Service

A Service gives Pods a stable network identity.

This matters because Pods can come and go. If I try to talk to a Pod directly, the address may change. A Service gives me one stable name and forwards traffic to the matching Pods.

That idea became very important later when I explained ClusterIP and internal service communication.


4. Kubernetes Architecture

Once I understood the pieces, I needed to understand how they connect.

Internet
   |
   v
Ingress Controller
   |
   v
Service
   |
   v
Pod
   |
   v
Container
Enter fullscreen mode Exit fullscreen mode

How the connection works

The request usually starts from the browser.

  1. The browser sends traffic to a host name such as posts.com
  2. The Ingress Controller receives the request
  3. Ingress routes the request to the correct Service
  4. The Service finds a matching Pod
  5. The Pod forwards the traffic to the container running the app

That chain is what made Kubernetes feel real to me. It is not random. It is a path.

A larger picture

                  +----------------------+
                  |   Kubernetes Cluster |
                  +----------+-----------+
                             |
           +-----------------+-----------------+
           |                                   |
           v                                   v
     Worker Node 1                        Worker Node 2
           |                                   |
        Pod(s)                              Pod(s)
           |                                   |
      Container(s)                       Container(s)
Enter fullscreen mode Exit fullscreen mode

The cluster decides where the Pods go. The nodes run them. Services give access to them. Ingress gives the outside world one entry point.


5. Deployments

Why Deployments exist

When I first saw a Deployment YAML, I wondered why I could not just create a Pod directly and stop there.

The answer is that Pods are temporary, but Deployments manage Pods for me.

A Deployment makes sure the desired number of Pod replicas exists. If a Pod dies, the Deployment creates a new one. If I want to update the app, it helps perform a rolling update.

Creating Pods the right way

I used Deployments instead of manually creating Pods because I wanted Kubernetes to manage the lifecycle.

For example, the Posts service deployment looks like this in spirit:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: posts-depl
spec:
  replicas: 1
  selector:
    matchLabels:
      app: posts
  template:
    metadata:
      labels:
        app: posts
    spec:
      containers:
        - name: posts
          image: toqeer43553/posts
Enter fullscreen mode Exit fullscreen mode

What each part means

  • apiVersion: apps/v1 means I am using the apps API group
  • kind: Deployment means this object manages Pods
  • metadata.name gives the Deployment a name
  • spec.replicas: 1 asks Kubernetes to keep one Pod running
  • spec.selector.matchLabels tells the Deployment which Pods belong to it
  • spec.template is the Pod template
  • spec.template.metadata.labels must match the selector
  • spec.template.spec.containers defines the container inside the Pod
  • image tells Kubernetes which image to run

Replica management

If I change replicas from 1 to 3, Kubernetes should keep three Pods alive.

That is useful when traffic grows. It is also useful when I want redundancy. If one Pod crashes, the others can still serve traffic.

Rolling updates

Rolling updates mean I can replace old Pods with new ones gradually.

That is safer than shutting down everything at once.

The benefit is simple:

  • fewer outages
  • less risk
  • easier rollback if something breaks

Self healing

Self healing was one of the most satisfying things to watch.

If I deleted a Pod manually, the Deployment created a replacement. That taught me the meaning of “desired state.” I say what I want. Kubernetes tries to keep it true.

Scaling

Scaling becomes easy once a Deployment owns the Pods.

Instead of managing each container by hand, I can ask for more replicas and let Kubernetes handle the rest.

Why I needed Deployments in this project

I had six app components. I did not want to manually babysit each one. Deployments gave me a controlled way to run them all and recover from failures.


6. Services

Services were one of the most important concepts I learned.

Why Services exist

Pods are temporary. Their IP addresses can change. That means one service cannot safely call another by Pod IP.

A Service gives me a stable name and a stable way to reach matching Pods.

Service types

I learned four common Service types.

ClusterIP

ClusterIP is the default service type. It exposes the service only inside the cluster.

I used ClusterIP for almost all internal services in this project.

Use it when:

  • only other services inside Kubernetes need access
  • you want internal service discovery
  • you do not want public access

Advantages:

  • secure by default
  • stable internal address
  • great for microservice-to-microservice communication

Disadvantages:

  • not reachable directly from outside the cluster

Real example:

  • posts-clusterip-srv
  • comments-srv
  • query-srv
  • event-bus-srv
  • moderation-srv

NodePort

NodePort exposes a service on a port on every node.

I used it for the Posts service in the project to help with external access in some learning setups.

Use it when:

  • you want a simple way to access a service from outside
  • you are learning or prototyping

Advantages:

  • simple to understand
  • easy to test locally

Disadvantages:

  • less flexible than Ingress
  • awkward for many services
  • not ideal for production

LoadBalancer

LoadBalancer asks a cloud provider to create an external load balancer.

Use it when:

  • you are in a cloud environment
  • you want a public entry point with managed infrastructure

Advantages:

  • simple cloud integration
  • external traffic routing is handled for you

Disadvantages:

  • depends on cloud support
  • not very useful in a pure local Minikube setup

ExternalName

ExternalName maps a service name to an external DNS name.

Use it when:

  • Kubernetes needs to refer to something outside the cluster

Advantages:

  • simple DNS aliasing
  • useful for external dependencies

Disadvantages:

  • does not act like normal proxy routing
  • not good for every use case

Real examples from my project

The internal services all use ClusterIP so that one service can talk to another by name.

For example:

http://event-bus-srv:5000/events
http://comments-srv:8001/events
http://query-srv:8002/events
Enter fullscreen mode Exit fullscreen mode

That was much cleaner than hardcoding IPs.

Why I chose ClusterIP for internal communication

Because the services talk to each other inside the cluster.

I did not want the browser or the public internet calling my internal moderation service directly. ClusterIP kept those services private while still making them reachable from inside the cluster.


7. Load Balancing

Why load balancing is important

If one service gets a lot of traffic, one Pod may not be enough. Load balancing spreads requests across available Pods.

That matters because it improves:

  • performance
  • reliability
  • availability

How Kubernetes balances traffic

When I send traffic to a Service, Kubernetes forwards it to one of the matching Pods.

If there are multiple replicas, the Service acts like a traffic distributor.

Client
  |
  v
Service
  |
  +-------> Pod 1
  +-------> Pod 2
  +-------> Pod 3
Enter fullscreen mode Exit fullscreen mode

What happens if one Pod crashes

If a Pod crashes, the Deployment replaces it.

If traffic is still flowing, the Service stops sending requests to the dead Pod and keeps using the healthy ones.

That gave me a lot more confidence in Kubernetes. It was not just running containers. It was actively keeping the system alive.

Simple example

If I had three replicas of the Query service, the Service could send requests to any healthy replica.

That means users would not care which exact Pod answered. They only care that the app works.


8. Communication Between Microservices

This is where the project became interesting.

The event flow I built

Posts Service
    |
    v
Event Bus
    |
    v
Query Service

Comments Service
    |
    v
Event Bus
    |
    +-------------------+
    |                   |
    v                   v
Moderation Service   Query Service
    |
    v
Event Bus
    |
    v
Comments Service
Enter fullscreen mode Exit fullscreen mode

The data does not move in a straight line like a normal form submit. It moves as events.

Why ClusterIP is used here

Because these services should talk privately inside the cluster.

The browser does not need to know where Moderation lives. The browser only talks to the public entry point. The internal services talk to each other through internal DNS names such as query-srv or event-bus-srv.

Why not use Pod IPs

Because Pod IPs are temporary.

If a Pod restarts, Kubernetes may assign a new IP. That would break hardcoded service addresses.

Using service names solves that problem.

Service discovery

Service discovery means services can find each other by name.

I did not need to ask, “What is the IP address of the Event Bus today?” I could just use the service DNS name.

That is a huge reason Kubernetes feels manageable once the basics click.

Internal DNS

Inside the cluster, a service name becomes a DNS entry.

That is why code like this works:

http://event-bus-srv:5000/events
Enter fullscreen mode Exit fullscreen mode

The service name is resolved inside Kubernetes, not by my laptop’s normal DNS setup.

Example from the project

When the Posts service creates a post, it stores it locally and then sends a PostCreated event to the Event Bus.

The Event Bus forwards that event to the Query service, which updates the read model.

That is the central idea:

  • write side changes data
  • event bus shares the change
  • read side updates asynchronously

That is event-driven microservices in plain language.


9. Ingress Controller

Why Ingress exists

Before Ingress, each service needed its own exposure strategy.

That becomes messy fast.

Ingress gives me one external entry point and lets me route requests based on host or path.

Problems before Ingress

Without Ingress, I would have needed:

  • multiple NodePorts
  • manual routing logic outside the cluster
  • more open ports
  • a harder mental model

That is not fun once a project grows.

One entry point

Ingress lets me say:

  • requests to /create-post go to the Posts service
  • requests to /get-posts go to the Query service
  • requests to /post/.../comments go to the Comments service
  • everything else goes to the React client

Host-based routing

The host in my setup is posts.com.

That means the app can be reached by a friendly domain instead of by a raw IP and port.

Path-based routing

Path-based routing means different URL paths go to different backend services.

Here is the idea in my project:

posts.com/create-post  -> Posts Service
posts.com/get-posts    -> Query Service
posts.com/post/...     -> Comments Service
posts.com/             -> Client Service
Enter fullscreen mode Exit fullscreen mode

My ingress configuration

The real Ingress manifest uses the nginx ingress class and a regex path for the comments route.

That setup lets Kubernetes route requests to the correct service without the browser needing to know internal service names.

How a request travels

Browser
  |
  v
posts.com/get-posts
  |
  v
Ingress Controller
  |
  v
query-srv
  |
  v
Query Pod
  |
  v
Response to browser
Enter fullscreen mode Exit fullscreen mode

That path was one of the clearest signs that I was learning real Kubernetes networking, not just memorizing YAML.


10. Host File Configuration

Why I edited the hosts file

My browser needs to know what posts.com means.

In local development, that name does not exist on the public internet. So I had to teach my laptop to map posts.com to the Minikube IP.

Example

127.0.0.1 posts.com
Enter fullscreen mode Exit fullscreen mode

Or, more commonly in Minikube setups:

<Minikube IP> posts.com
Enter fullscreen mode Exit fullscreen mode

Why browsers need this

When I type a domain into the browser, the browser asks DNS where to send the request.

If the domain is only for local development, I need a manual mapping. The hosts file gives me that mapping.

What I learned

This part felt small at first, but it was very important.

If the host file is wrong, Ingress may be working perfectly and the browser will still fail. That made me realize how many layers exist between “type URL” and “see page.”


11. Minikube

What is Minikube?

Minikube is a local Kubernetes cluster.

It lets me practice Kubernetes on my own machine without needing a cloud account or a production cluster.

Why use Minikube

I used Minikube because it made learning possible.

It gave me a safe place to test things like:

  • Deployments
  • Services
  • Ingress
  • Pods
  • DNS
  • local image handling

Local Kubernetes

Minikube is Kubernetes, but small and local.

That was perfect for me because I could test, break, and retry quickly.

Cluster creation

I learned that starting a cluster is the first step before applying manifests.

minikube start
Enter fullscreen mode Exit fullscreen mode

That command gives me a local cluster to work with.

Cluster deletion

When I wanted a clean restart, I could delete the cluster and create it again.

minikube delete
Enter fullscreen mode Exit fullscreen mode

That saved me many times when I had a messy local state.

Status

I used status checks to make sure the cluster was healthy before blaming my YAML.

Dashboard

The Minikube dashboard helped me see what was running.

Seeing Pods, Services, and Deployments in a UI made the system much less abstract.

Accessing services

Minikube also helped me access Services through IPs and tunnel-like behavior depending on the setup.

That became especially important when I started learning NodePort, Ingress, and the hosts file.


12. Minikube IP

Why Minikube has its own IP

Minikube runs as its own local Kubernetes environment. It is not the same thing as my laptop’s localhost.

That means the cluster may have a separate IP address.

localhost vs Minikube IP

localhost means my own machine.

Minikube IP means the address where the Minikube cluster can be reached.

These are sometimes related, but they are not the same thing.

Why NodePort works

NodePort exposes a port on the node.

If I know the Minikube IP and the NodePort, I can reach the service from outside the cluster.

Why localhost sometimes works

In some setups, Minikube is close enough to my local environment that localhost can work through port forwarding or driver-specific behavior.

That made me realize there is no single universal local Kubernetes setup. The exact access path depends on the driver and cluster configuration.

Internal networking

Inside the cluster, Services and Pods talk on the internal Kubernetes network.

Outside the cluster, I need Ingress, NodePort, port forwarding, or another access method.

Simple diagram

Laptop browser
    |
    v
localhost or Minikube IP
    |
    v
NodePort / Ingress
    |
    v
Service
    |
    v
Pod
    |
    v
Container
Enter fullscreen mode Exit fullscreen mode

That separation helped me understand why some URLs worked and some did not.


13. Docker Inside Minikube

How Docker works inside Minikube

This was a painful but useful lesson.

I first built images on my laptop and expected Minikube to see them automatically.

That is because Minikube often uses its own image environment.

Why images are built inside Minikube

If Kubernetes inside Minikube tries to pull an image, it needs that image to exist in a place it can access.

When I build locally, my host Docker may have the image. But Minikube’s cluster may not see it unless I either push it to a registry or build it in Minikube’s Docker environment.

Why Kubernetes cannot see local Docker images

This was one of my most confusing early mistakes.

I had the image on my machine, so I assumed Kubernetes would just use it. But the cluster is not my laptop shell. The cluster is its own environment.

That is why image visibility matters.

docker-env

The Minikube Docker environment helps me build images directly into Minikube’s image store.

The command looks like this:

eval $(minikube docker-env)
Enter fullscreen mode Exit fullscreen mode

Why this helps

After running that command, my Docker CLI talks to Minikube’s Docker daemon instead of my local one.

That means when I run docker build, the image is built where Kubernetes can see it.

Simple explanation

Before:

My laptop Docker -> local image only
Minikube -> cannot see it
Enter fullscreen mode Exit fullscreen mode

After:

My laptop Docker CLI -> Minikube Docker daemon -> image available to Kubernetes
Enter fullscreen mode Exit fullscreen mode

What I learned

I stopped thinking of Docker as one global thing. It is really a client talking to a Docker environment.

That small realization made Minikube much easier to work with.


14. Skaffold

Skaffold was the tool that made the whole workflow feel smoother.

What problem Skaffold solves

Without Skaffold, my workflow looked like this:

edit code
 -> docker build
 -> docker push
 -> kubectl apply
 -> check rollout
 -> repeat
Enter fullscreen mode Exit fullscreen mode

That is slow and annoying during development.

How Skaffold automates everything

Skaffold watches files, rebuilds images, and redeploys Kubernetes resources for me.

That means I can focus on changing code instead of repeating the same commands all day.

skaffold dev

This command starts a development loop.

It watches for file changes and updates the cluster.

That was huge for productivity because I could edit, save, and see changes with much less manual work.

Image rebuild

When source files change, Skaffold can rebuild the matching image.

File watching

It watches my app files so I do not have to trigger everything manually.

Sync

For some files, Skaffold can sync changes directly into a running container instead of rebuilding every time.

That makes the feedback loop faster.

Auto deployment

When images change, Skaffold applies the Kubernetes manifests again.

That closes the loop between code and cluster.

Hot reload

In practice, it feels close to hot reload for the whole stack, even though the exact behavior depends on the app and file syncing setup.

My Skaffold YAML

The config in this project defines:

  • the Kubernetes manifests to deploy
  • the local build settings
  • the images for client and each service
  • the sync rules for JavaScript files

That was a clean way to keep development repeatable.

Why Skaffold mattered so much

It reduced friction.

When a workflow gets too manual, I spend more time operating tools than learning the system. Skaffold pushed me back toward learning the app itself.


15. Important Commands Explained

This is a compact cheat sheet for the commands I kept using.

kubectl commands

  • kubectl get pods: shows Pods.
  • kubectl get services: shows Services.
  • kubectl get deployments: shows Deployments.
  • kubectl describe pod: shows Pod events.
  • kubectl logs: prints logs.
  • kubectl delete pod: removes a Pod.
  • kubectl apply -f: applies YAML.
  • kubectl delete -f: deletes YAML resources.
  • kubectl rollout restart: restarts a Deployment.
  • kubectl exec: runs a command inside a container.
  • kubectl port-forward: forwards a local port.
  • kubectl get nodes: shows cluster nodes.
  • kubectl get ingress: shows Ingress resources.
  • kubectl describe ingress: helps debug routing.

Example:

kubectl apply -f infra/k8s
Enter fullscreen mode Exit fullscreen mode

Minikube commands

  • minikube start: starts the cluster.
  • minikube stop: pauses the cluster.
  • minikube delete: resets the cluster.
  • minikube ip: prints the cluster IP.
  • minikube dashboard: opens the dashboard.
  • minikube docker-env: prints Docker settings.
  • eval $(minikube docker-env): points my shell at Minikube Docker.

Skaffold commands

  • skaffold dev: starts watch mode.
  • skaffold run: does one build and deploy.

Docker commands

  • docker build: creates an image.
  • docker images: shows local images.
  • docker ps: shows running containers.
  • docker logs: shows container output.
  • docker exec: runs a command inside a container.
  • docker tag: renames an image.
  • docker push: uploads an image.

Once I knew these commands, I could move from confusion to inspection much faster.


16. Common Problems I Faced

This project taught me that Kubernetes is full of small mistakes that look big at first.

Pods not starting

Usually this happened because of an image issue, a bad manifest, or a port mismatch.

How I debugged it:

  • ran kubectl get pods
  • used kubectl describe pod
  • checked kubectl logs

ImagePullBackOff

This often meant Kubernetes could not find the image.

My fix was usually one of these:

  • build the image in Minikube Docker
  • make sure the image name matches the manifest
  • ensure the tag is correct

CrashLoopBackOff

This usually meant the container started and then crashed repeatedly.

Common causes:

  • app error
  • missing environment assumptions
  • wrong port
  • bad startup code

Service not found

This usually meant I typed the wrong service name or expected the wrong DNS name.

I learned to compare the service name in YAML with the one used in code.

Ingress not working

This happened when the ingress controller was missing, the host file was wrong, or the route path did not match.

I learned to check all three layers:

  • Ingress resource exists
  • ingress controller is running
  • browser hostname resolves correctly

Hosts file issues

If the host mapping was wrong, nothing else mattered.

That was a reminder that local networking issues can look like app issues.

Minikube IP changed

When the Minikube IP changed, my hosts file mapping became stale.

The fix was to run minikube ip again and update the mapping.

Docker image not updating

This happened when I changed code but the cluster kept using an old image.

The fix was usually:

  • rebuild the image
  • use Skaffold
  • verify Minikube Docker context

Skaffold not rebuilding

This usually meant the file sync rules were too narrow or the config did not match the file location.

I learned to check the sync paths carefully.

My debugging habit

The biggest lesson was not to guess.

I tried to inspect one layer at a time:

  1. Is the Pod running?
  2. Is the Service correct?
  3. Is the Ingress correct?
  4. Does the host file point to the right place?
  5. Does the app log show errors?

That simple checklist saved me a lot of time.


17. Pros and Cons

Pros of Kubernetes

  • It keeps services running and replaces failed Pods
  • It supports service discovery
  • It works from local dev to production

Cons of Kubernetes

  • It has a steep learning curve
  • YAML can be tedious
  • Local setups can differ from production

Pros of Skaffold

  • It speeds up development
  • It reduces manual commands
  • It watches files and rebuilds automatically

Cons of Skaffold

  • The config can be confusing at first
  • Sync rules need care
  • It adds another tool to learn

Pros of Ingress

  • One entry point for many services
  • Clean routing by host and path

Cons of Ingress

  • Needs an ingress controller
  • Debugging can be tricky
  • Host and path rules must match exactly

Pros of Microservices

  • Clear service boundaries
  • Easier to scale parts separately
  • Better for event-driven systems

Cons of Microservices

  • More moving parts
  • More networking problems
  • More concepts to learn

18. Lessons I Learned

This is the part that matters most to me.

At first, Kubernetes felt like too many objects with strange names. What helped was not reading more definitions. What helped was building something real.

Once I saw the posts app create an event, then watched the query model update, then saw comments go through moderation, I started to understand why these parts exist.

The biggest mental shift was this:

  • Docker runs containers
  • Kubernetes manages them
  • Services make them reachable
  • Ingress makes them public in a controlled way
  • Skaffold makes development faster

I also learned that microservices are not just about splitting code. They are about splitting responsibility.

Another thing that became easier was reading YAML. At first it felt dry and repetitive, but later I saw it as a contract between me and Kubernetes.


19. Best Practices

Here are the beginner-friendly practices that helped me most.

Keep services small

Each service should have one clear job.

Use service names, not Pod IPs

Pod IPs can change. Service names are stable.

Separate write and read paths when it makes sense

The CQRS idea helped my app stay organized.

Use ClusterIP for internal services

Do not expose everything publicly.

Use Ingress for the public entry point

This keeps routing clean.

Check logs early

Logs often tell the truth faster than guesswork.

Learn the manifest one piece at a time

Do not try to memorize everything at once.

Keep your local environment clean

If Minikube becomes messy, reset it instead of fighting stale state forever.

Use Skaffold during development

It saves time and keeps the feedback loop short.

Understand the path of a request

Always ask:

  • Where did the request start?
  • Which service received it?
  • Which Service forwarded it?
  • Which Pod handled it?

That question alone will save you a lot of confusion.


20. Conclusion

I started this project wanting to learn Kubernetes. I ended up learning much more.

I learned how containers package software.

I learned how Kubernetes runs those containers across a cluster.

I learned why Deployments matter, why Services matter, why Ingress matters, and why local tools like Minikube and Skaffold make practice possible.

Most of all, I learned that Kubernetes becomes easier when it is tied to a real project.

On their own, the concepts can feel abstract. But when I used them to run a real microservices app, they started to make sense.

If you are a beginner, do not try to understand everything in one day. Build something small, break it, and repeat.

That is how the pieces connect.


21. What’s Next?

This project gave me a good base, but I know there is more to learn.

The next topics I want to study are:

  • ConfigMaps
  • Secrets
  • Persistent Volumes
  • Helm
  • Horizontal Pod Autoscaler
  • Monitoring
  • Logging
  • CI/CD
  • Production Kubernetes
  • Cloud Kubernetes such as AWS EKS, GKE, and AKS

I feel more ready for those topics now because I understand the basics from a real app, not just from theory.


SEO Details

SEO title

Building a Microservices App with Kubernetes: My Beginner-Friendly Journey Through Docker, Minikube, Ingress, and Skaffold

Meta description

A beginner-friendly story of building a microservices application with Kubernetes, covering Docker, Pods, Deployments, Services, Ingress, Minikube, Docker inside Minikube, and Skaffold in simple English.

Suggested URL slug

building-microservices-app-with-kubernetes-docker-minikube-ingress-skaffold

Dev.to tags

  • kubernetes
  • docker
  • microservices
  • devops
  • beginners
  • minikube
  • skaffold
  • ingress
  • nodejs
  • react

Suggested cover image idea

A clean illustration of a laptop connected to a Kubernetes cluster, with small boxes labeled Docker, Pods, Services, Ingress, and Skaffold, styled like a simple system map.

Top comments (0)