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
- Introduction to the Project
- Understanding Containers
- Kubernetes Basics
- Kubernetes Architecture
- Deployments
- Services
- Load Balancing
- Communication Between Microservices
- Ingress Controller
- Host File Configuration
- Minikube
- Minikube IP
- Docker Inside Minikube
- Skaffold
- Important Commands Explained
- Common Problems I Faced
- Pros and Cons
- Lessons I Learned
- Best Practices
- Conclusion
- What’s Next?
- 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
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
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
How the connection works
The request usually starts from the browser.
- The browser sends traffic to a host name such as posts.com
- The Ingress Controller receives the request
- Ingress routes the request to the correct Service
- The Service finds a matching Pod
- 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)
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
What each part means
-
apiVersion: apps/v1means I am using the apps API group -
kind: Deploymentmeans this object manages Pods -
metadata.namegives the Deployment a name -
spec.replicas: 1asks Kubernetes to keep one Pod running -
spec.selector.matchLabelstells the Deployment which Pods belong to it -
spec.templateis the Pod template -
spec.template.metadata.labelsmust match the selector -
spec.template.spec.containersdefines the container inside the Pod -
imagetells 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-srvcomments-srvquery-srvevent-bus-srvmoderation-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
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
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
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
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-postgo to the Posts service - requests to
/get-postsgo to the Query service - requests to
/post/.../commentsgo 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
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
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
Or, more commonly in Minikube setups:
<Minikube IP> posts.com
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
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
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
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)
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
After:
My laptop Docker CLI -> Minikube Docker daemon -> image available to Kubernetes
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
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
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:
- Is the Pod running?
- Is the Service correct?
- Is the Ingress correct?
- Does the host file point to the right place?
- 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)