{"id":37,"date":"2025-10-27T12:00:00","date_gmt":"2025-10-27T12:00:00","guid":{"rendered":"https:\/\/rezachegini.com\/?p=37"},"modified":"2026-10-30T06:39:14","modified_gmt":"2026-10-30T06:39:14","slug":"putting-everything-together-my-first-kubernetes-deployment","status":"publish","type":"post","link":"https:\/\/rezachegini.com\/?p=37","title":{"rendered":"Putting Everything Together: My First Kubernetes Deployment"},"content":{"rendered":"<p class=\"wp-block-paragraph\">After learning about pods, deployments, and services, I thought I finally understood Kubernetes. But to be honest, I was mostly just following tutorials and copying code. I wanted to build something real to see if I truly understood how everything worked together.<\/p>\n<p class=\"wp-block-paragraph\">You know that moment when you realize theory is easier than practice? That was me. I could explain what a pod was, but deploying a real app with all the pieces connected was a whole new challenge.<\/p>\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/github.com\/mohammadrezachegini\/home-lab\/tree\/master\/K8S\/K8S-Fundamentals\/Mealie-deployments-app\">Github Link<\/a><\/p>\n<h3 class=\"wp-block-heading\">The Challenge<\/h3>\n<ul class=\"wp-block-list\"><li>I needed to figure out how to make three things work together:<\/li><li>The application itself (deployment)<\/li><li>Storage for my recipes (persistent volume)<\/li><\/ul>\n<h3 class=\"wp-block-heading\">The Four Files I Created<\/h3>\n<p class=\"wp-block-paragraph\">To deploy Mealie properly, I needed four configuration files. Let me explain each one:<\/p>\n<h3 class=\"wp-block-heading\">0. Namespace File (mealie-namespace.yaml)<\/h3>\n<p class=\"wp-block-paragraph\">First, I learned I should create the namespace properly instead of just using kubectl commands.<\/p>\n<pre class=\"wp-block-code\"><code>apiVersion: v1\nkind: Namespace\nmetadata:\n  name: mealie\n  labels:\n    name: mealie<\/code><\/pre>\n<p class=\"wp-block-paragraph\">This is simple but important:<\/p>\n<p class=\"wp-block-paragraph\">apiVersion: v1 &#8211; Namespaces use the basic v1 API.<\/p>\n<p class=\"wp-block-paragraph\">kind: Namespace &#8211; Creates a namespace to organize everything.<\/p>\n<p class=\"wp-block-paragraph\">metadata:<\/p>\n<ul class=\"wp-block-list\"><li>name: mealie &#8211; The namespace name that I reference in all other files<\/li><li>labels: name: mealie &#8211; Good practice to label namespaces for organization<\/li><\/ul>\n<p class=\"wp-block-paragraph\">I could have just run kubectl create namespace mealie but having it as a file means I can track it in git and recreate everything easily. Plus it looks more professional when everything is documented.<\/p>\n<h3 class=\"wp-block-heading\">1. Storage File (mealie-storage.yaml)<\/h3>\n<p class=\"wp-block-paragraph\">This file creates storage space for recipe data.<\/p>\n<pre class=\"wp-block-code\"><code>apiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: mealie-data\n  namespace: mealie\nspec:\n  accessModes:\n    - ReadWriteOnce\n  resources:\n    requests:\n      storage: 500Mi<\/code><\/pre>\n<p class=\"wp-block-paragraph\">Let me break this down line by line because I wish someone had explained it this way when I started:<\/p>\n<p class=\"wp-block-paragraph\">apiVersion: v1 &#8211; This tells Kubernetes which API version to use. For storage, it&#8217;s always v1. Different resources use different API versions, which confused me at first.<\/p>\n<p class=\"wp-block-paragraph\">kind: PersistentVolumeClaim &#8211; This is like asking Kubernetes &#8220;hey, I need some storage space&#8221;. It&#8217;s not creating storage directly, but requesting it from available storage in the cluster.<\/p>\n<p class=\"wp-block-paragraph\">metadata section:<\/p>\n<ul class=\"wp-block-list\"><li>name: mealie-data &#8211; I can call this whatever I want. This name is what I will reference later in my deployment file.<\/li><li>namespace: mealie &#8211; Keeps everything organized in its own space. Without this, it goes to the default namespace.<\/li><\/ul>\n<p class=\"wp-block-paragraph\">spec section:<\/p>\n<ul class=\"wp-block-list\"><li>accessModes: ReadWriteOnce &#8211; This means only one pod can mount this storage at a time. I tried ReadWriteMany first but my cluster doesn&#8217;t support it, so I learned this.<\/li><li>storage: 500Mi &#8211; I started with 100Mi but realized recipe photos take up space, so I increased it. You can always request more later if needed.<\/li><\/ul>\n<h3 class=\"wp-block-heading\">2. Deployment File (mealie-deployment.yaml)<\/h3>\n<p class=\"wp-block-paragraph\">This file tells Kubernetes how to run the Mealie application.<\/p>\n<pre class=\"wp-block-code\"><code>apiVersion: apps\/v1\nkind: Deployment\nmetadata:\n  labels:\n    app: mealie\n  name: mealie\n  namespace: mealie\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: mealie\n  template:\n    metadata:\n      labels:\n        app: mealie\n    spec:\n      containers:\n      - image: ghcr.io\/mealie-recipes\/mealie:nightly\n        name: reza-mealie\n        ports:\n          - containerPort: 9000\n        volumeMounts:\n          - mountPath: \/app\/data\n            name: mealie-data\n      volumes:\n        - name: mealie-data\n          persistentVolumeClaim:\n            claimName: mealie-data<\/code><\/pre>\n<p class=\"wp-block-paragraph\">This file is where the magic happens. Let me explain each part because this one took me the longest to understand:<\/p>\n<p class=\"wp-block-paragraph\">apiVersion: apps\/v1 &#8211; Deployments use the &#8220;apps\/v1&#8221; API, different from storage which uses &#8220;v1&#8221;.<\/p>\n<p class=\"wp-block-paragraph\">kind: Deployment &#8211; This creates a deployment, which manages pods for me. Much better than creating pods manually.<\/p>\n<p class=\"wp-block-paragraph\">metadata section:<\/p>\n<ul class=\"wp-block-list\"><li>labels: app: mealie &#8211; These labels are super important. They help other resources find this deployment.<\/li><li>name: mealie &#8211; The deployment name. I keep it simple and consistent.<\/li><li>namespace: mealie &#8211; Same namespace as my storage.<\/li><\/ul>\n<p class=\"wp-block-paragraph\">spec section (the main configuration):<\/p>\n<p class=\"wp-block-paragraph\">replicas: 1 &#8211; I am just testing with one copy..<\/p>\n<p class=\"wp-block-paragraph\">selector &#8211; This is how the deployment finds its pods. The matchLabels must match the labels in the template below. I messed this up the first time and wondered why nothing worked.<\/p>\n<p class=\"wp-block-paragraph\">template &#8211; This is the blueprint for creating pods:<\/p>\n<p class=\"wp-block-paragraph\">Container configuration:<\/p>\n<ul class=\"wp-block-list\"><li>image: <a href=\"http:\/\/ghcr.io\/mealie-recipes\/mealie:nightly\">ghcr.io\/mealie-recipes\/mealie:nightly<\/a> &#8211; I use the nightly version to get latest features. You might prefer stable versions in production.<\/li><li>name: reza-mealie &#8211; Just the container name, can be anything.<\/li><li>containerPort: 9000 &#8211; The port inside the container where Mealie runs.<\/li><\/ul>\n<p class=\"wp-block-paragraph\">Storage connection (the tricky part):<\/p>\n<ul class=\"wp-block-list\"><li>volumeMounts &#8211; This connects storage to the container at \/app\/data path<\/li><li>volumes &#8211; This references the storage I created earlier by name &#8220;mealie-data&#8221;<\/li><\/ul>\n<p class=\"wp-block-paragraph\">The tricky part was connecting the storage I created earlier. See the &#8220;volumeMounts&#8221; section? That links the storage to the app at the \/app\/data folder. Without this connection, my recipes would disappear every time the pod restarted.<\/p>\n<p class=\"wp-block-paragraph\">The volume name in volumeMounts must match the volume name in the volumes section. And the claimName must match the storage I created in the first file. It&#8217;s like a chain, everything has to connect properly.<\/p>\n<h3 class=\"wp-block-heading\">3. Service File (service.yaml)<\/h3>\n<p class=\"wp-block-paragraph\">This file makes the application available to users on the internet.<\/p>\n<pre class=\"wp-block-code\"><code>apiVersion: v1\nkind: Service\nmetadata:\n  labels:\n    app: mealie\n  name: mealie\n  namespace: mealie\nspec:\n  ports:\n  - port: 9000\n    protocol: TCP\n    targetPort: 9000\n  selector:\n    app: mealie\n  type: LoadBalancer<\/code><\/pre>\n<p class=\"wp-block-paragraph\">This was the part that finally made everything accessible. Let me explain each piece:<\/p>\n<p class=\"wp-block-paragraph\">apiVersion: v1 &#8211; Services use the basic v1 API, same as storage.<\/p>\n<p class=\"wp-block-paragraph\">kind: Service &#8211; This creates a network service to expose my app.<\/p>\n<p class=\"wp-block-paragraph\">metadata section:<\/p>\n<ul class=\"wp-block-list\"><li>labels and name &#8211; Same pattern as before, keeping everything consistent.<\/li><li>namespace: mealie &#8211; Must be in the same namespace as my deployment.<\/li><\/ul>\n<p class=\"wp-block-paragraph\">spec section:<\/p>\n<p class=\"wp-block-paragraph\">ports configuration:<\/p>\n<ul class=\"wp-block-list\"><li>port: 9000 &#8211; This is the port users connect to from outside<\/li><li>targetPort: 9000 &#8211; This is the port on my container (must match containerPort in deployment)<\/li><li>protocol: TCP &#8211; Most web apps use TCP protocol<\/li><\/ul>\n<p class=\"wp-block-paragraph\">selector: app: mealie &#8211; This is crucial! It tells the service which pods to send traffic to. It matches the labels in my deployment. If this doesn&#8217;t match, nothing works.<\/p>\n<p class=\"wp-block-paragraph\">type: LoadBalancer &#8211; This gives me an external IP address. Other options are:<\/p>\n<ul class=\"wp-block-list\"><li>ClusterIP (internal only)<\/li><li>NodePort (exposes on node IP)<\/li><li>LoadBalancer (external access)<\/li><\/ul>\n<p class=\"wp-block-paragraph\">The LoadBalancer type gives me an external IP address. In my home lab setup, this means I can access the recipes from any device on my network. Pretty cool!<\/p>\n<h3 class=\"wp-block-heading\">How They Work Together<\/h3>\n<p class=\"wp-block-paragraph\">Here is how all four files work as a team:<\/p>\n<ol class=\"wp-block-list\"><li>Namespace creates an organized space for everything<\/li><li>Storage creates a safe place to keep recipe data<\/li><li>Deployment runs the Mealie application and connects it to storage<\/li><li>Service provides network access so users can reach the app<\/li><\/ol>\n<h3 class=\"wp-block-heading\">How to Deploy Everything<\/h3>\n<p class=\"wp-block-paragraph\">Here are the commands I used to deploy everything:<\/p>\n<pre class=\"wp-block-code\"><code># Create the namespace first\nkubectl apply -f mealie-namespace.yaml\n\n# Apply the storage configuration\nkubectl apply -f mealie-storage.yaml\n\n# Deploy the application\nkubectl apply -f mealie-deployment.yaml\n\n# Create the service for network access\nkubectl apply -f service.yaml\n\n# Check everything is working\nkubectl get all -n mealie<\/code><\/pre>\n<p class=\"wp-block-paragraph\">The order is important. I create the namespace first, then storage, then the application, then the network service. Having the namespace as a file means I can track everything in git and recreate it easily.<\/p>\n<h3 class=\"wp-block-heading\">What Actually Happened<\/h3>\n<p class=\"wp-block-paragraph\">My first attempt failed completely. I forgot to create the storage first, so when I added recipes and restarted the pod, everything disappeared. That was frustrating but taught me about the importance of doing things in the right order.<\/p>\n<p class=\"wp-block-paragraph\">The second time went much better. Now I have my recipes organized and accessible from anywhere in my home network.<\/p>\n<h3 class=\"wp-block-heading\">What I Learned<\/h3>\n<p class=\"wp-block-paragraph\">This project connected all the concepts from my previous posts. Those pods I learned about? They run inside deployments. Those services? They actually route traffic to real applications. Everything clicked together.<\/p>\n<h3 class=\"wp-block-heading\">Next Steps<\/h3>\n<p class=\"wp-block-paragraph\">I want to try adding multiple replicas next time and maybe set up ingress for proper domain access. Also thinking about monitoring.<\/p>\n<p class=\"wp-block-paragraph\">It would be nice to know if something goes wrong.<\/p>","protected":false},"excerpt":{"rendered":"<p>After learning about pods, deployments, and services, I thought I finally understood Kubernetes. But to be honest, I was mostly just following&hellip;<\/p>\n","protected":false},"author":2,"featured_media":39,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_rezaops_part":"5","_rezaops_linkedin_url":"https:\/\/www.linkedin.com\/pulse\/putting-everything-together-my-first-kubernetes-reza-chegini-geouc\/","footnotes":""},"categories":[10,9],"tags":[13,8,5,3,7,6],"series":[11],"class_list":["post-37","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops","category-kubernetes","tag-deployments","tag-devops","tag-kubectl","tag-kubernetes","tag-kubernetes-services","tag-yaml","series-understanding-kubernetes"],"jetpack_featured_media_url":"https:\/\/rezachegini.com\/wp-content\/uploads\/2026\/10\/putting-everything-together-kubernetes-cover.png","_links":{"self":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts\/37","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=37"}],"version-history":[{"count":1,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts\/37\/revisions"}],"predecessor-version":[{"id":38,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts\/37\/revisions\/38"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/media\/39"}],"wp:attachment":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=37"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=37"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=37"},{"taxonomy":"series","embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fseries&post=37"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}