{"id":155,"date":"2026-06-13T12:00:00","date_gmt":"2026-06-13T12:00:00","guid":{"rendered":"https:\/\/rezachegini.com\/?p=155"},"modified":"2026-10-30T07:05:51","modified_gmt":"2026-10-30T07:05:51","slug":"part-01-why-i-built-a-home-lab-os-setup-system-hardening","status":"publish","type":"post","link":"https:\/\/rezachegini.com\/?p=155","title":{"rendered":"Part 01 &#8211; Why I Built a Home Lab + OS Setup &#038; System Hardening"},"content":{"rendered":"\n<h3 class=\"wp-block-heading\">Why I Built a Home Lab<\/h3>\n\n\n<p class=\"wp-block-paragraph\">I&#8217;m a Junior DevOps and Cloud Engineer , actively building my skills across Kubernetes, CI\/CD, observability, and infrastructure automation.<\/p>\n\n\n<p class=\"wp-block-paragraph\">Like many engineers, I had certifications, AWS, GCP, but I kept running into the same problem during interviews and job applications: I could talk about the tools, but I didn&#8217;t have enough hands-on depth with real infrastructure.<\/p>\n\n\n<p class=\"wp-block-paragraph\">Spinning up a managed EKS or GKE cluster is easy. But what happens when a node goes down? When DNS breaks? When a pod can&#8217;t pull an image? When your storage class isn&#8217;t the default? These are the things that happen in production every day. and you only learn to handle them by actually breaking things and fixing them yourself.<\/p>\n\n\n<p class=\"wp-block-paragraph\">So I decided to build a real home lab. Not a VM on my laptop. Not a single-node cluster. A proper two-node bare metal Kubernetes cluster running a full DevOps stack. the same tools used in production environments at real companies.<\/p>\n\n\n<p class=\"wp-block-paragraph\">This article is Part 1 of a 13-part series documenting everything I built, every YAML file I wrote, and every error I hit along the way.<\/p>\n\n\n<h3 class=\"wp-block-heading\">Hardware Choices<\/h3>\n\n\n<p class=\"wp-block-paragraph\">I wanted real machines, not cloud VMs, because bare metal forces you to deal with things that managed services abstract away: networking, storage, DNS, node failures.<\/p>\n\n\n<p class=\"wp-block-paragraph\">After researching mini PCs (low power, quiet, affordable), I settled on two machines:<\/p>\n\n\n<h3 class=\"wp-block-heading\">Node 1 &#8211; jarvis (Control Plane)<\/h3>\n\n\n<p class=\"wp-block-paragraph\">MINISFORUM UM773 Lite<\/p>\n\n\n<ul class=\"wp-block-list\">\n<li>CPU \u2192 AMD Ryzen 7 7735HS (8C\/16T, 4.75GHz)<\/li>\n\n\n<li>RAM \u2192 32GB DDR5<\/li>\n\n\n<li>Storage \u2192 1TB PCIe 4.0 NVMe SSD<\/li>\n\n\n<li>GPU \u2192 AMD Radeon 680M (integrated)<\/li>\n\n\n<li>Network \u2192 WiFi 6 \u00b7 Bluetooth 5.2<\/li>\n\n\n<li>Ports \u2192 2x HDMI \u00b7 1x USB4 \u00b7 5x USB<\/li>\n<\/ul>\n\n\n<p class=\"wp-block-paragraph\">Why this machine for control plane? The 32GB RAM is the key factor. Running Prometheus, Grafana, Loki, Jenkins, ArgoCD, and Vault simultaneously requires serious memory headroom. The control plane stack alone can consume 8\u201312GB under normal load.<\/p>\n\n\n<h3 class=\"wp-block-heading\">Node 2: thor (Worker Node + Dev Machine)<\/h3>\n\n\n<p class=\"wp-block-paragraph\">MINISFORUM M1 Plus<\/p>\n\n\n<ul class=\"wp-block-list\">\n<li>CPU \u2192 AMD Ryzen 7 7735HS (8C\/16T, 4.75GHz)<\/li>\n\n\n<li>RAM \u2192 32GB DDR5<\/li>\n\n\n<li>Storage \u2192 1TB PCIe 4.0 NVMe SSD<\/li>\n\n\n<li>GPU \u2192 AMD Radeon 680M (integrated)<\/li>\n\n\n<li>Network \u2192 WiFi 6 \u00b7 Bluetooth 5.2<\/li>\n\n\n<li>Ports \u2192 2x HDMI \u00b7 1x USB4 \u00b7 5x USB<\/li>\n<\/ul>\n\n\n<p class=\"wp-block-paragraph\">Why this machine for worker? The i5-12600H has 4 performance cores + 8 efficiency cores, perfect for bursty CI\/CD workloads and running multiple app containers concurrently. I also use thor as my development environment via VS Code SSH Remote. so I develop, test, and deploy all from the same machine.<\/p>\n\n<h3 class=\"wp-block-heading\">Why Not Use Cloud VMs Instead?<\/h3>\n\n\n<ul class=\"wp-block-list\">\n<li>Cost: Running 2 equivalent VMs 24\/7 on AWS would cost $100\u2013200\/month. These mini PCs were a one-time purchase.<\/li>\n\n\n<li>Real networking: On bare metal I deal with real DNS, real firewall rules, real network interfaces. Cloud abstracts all of this.<\/li>\n\n\n<li>Learning: Every error on bare metal teaches you something. MetalLB, CoreDNS, Longhorn, none of these exist in managed Kubernetes because the cloud handles it for you.<\/li>\n<\/ul>\n\n\n<h3 class=\"wp-block-heading\">Architecture Overview<\/h3>\n\n\n<pre class=\"wp-block-code\"><code>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502                    HOME NETWORK (WiFi)                   \u2502\n\u2502                     10.0.0.0\/24                         \u2502\n\u2502                                                         \u2502\n\u2502  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n\u2502  \u2502  jarvis (10.0.0.148) \u2502  \u2502   thor (10.0.0.149)      \u2502 \u2502\n\u2502  \u2502  UM773 \u00b7 32GB        \u2502  \u2502   M1 Plus \u00b7 16GB         \u2502 \u2502\n\u2502  \u2502                      \u2502  \u2502                          \u2502 \u2502\n\u2502  \u2502  K3s Control Plane   \u2502  \u2502   K3s Worker Node        \u2502 \u2502\n\u2502  \u2502  Prometheus \u00b7 Grafana\u2502  \u2502   App workloads          \u2502 \u2502\n\u2502  \u2502  Loki \u00b7 Jaeger       \u2502  \u2502   Dev environment        \u2502 \u2502\n\u2502  \u2502  Jenkins \u00b7 ArgoCD    \u2502  \u2502   Longhorn replica       \u2502 \u2502\n\u2502  \u2502  Vault \u00b7 CoreDNS     \u2502  \u2502                          \u2502 \u2502\n\u2502  \u2502  Registry \u00b7 Homepage \u2502  \u2502                          \u2502 \u2502\n\u2502  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n\u2502                                                         \u2502\n\u2502  MetalLB IP Pool: 10.0.0.200 \u2013 10.0.0.220              \u2502\n\u2502  Ingress IP:      10.0.0.200                            \u2502\n\u2502  CoreDNS IP:      10.0.0.209                            \u2502\n\u2502  Registry IP:     10.0.0.206                            \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n         \u2502\n         \u2502 SSH \/ kubectl \/ browser\n         \u2502\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502  MacBook M2 Pro    \u2502\n\u2502  kubectl, Helm,    \u2502\n\u2502  Terraform, SSH    \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518<\/code><\/pre>\n\n\n<h3 class=\"wp-block-heading\">Full Stack: What&#8217;s Running and Why<\/h3>\n\n\n<ol class=\"wp-block-list\">\n<li>Infrastructure K3s \u00b7 Lightweight Kubernetes distribution MetalLB \u00b7 Assigns real LAN IPs to LoadBalancer services NGINX Ingress \u00b7 Routes HTTP traffic to pods by hostname Longhorn \u00b7 Distributed block storage across both nodes<\/li>\n\n\n<li>Observability Prometheus \u00b7 Scrapes metrics from all pods and nodes Grafana \u00b7 Dashboards and visualization Loki \u00b7 Log aggregation Promtail \u00b7 Ships pod logs to Loki Jaeger \u00b7 Distributed tracing<\/li>\n\n\n<li>CI\/CD Jenkins \u00b7 Builds Docker images and runs pipelines Registry \u00b7 Self-hosted container image registry<\/li>\n\n\n<li>GitOps &amp; Secrets ArgoCD \u00b7 Syncs K8s manifests from GitHub automatically Vault \u00b7 Secrets management External Secrets \u00b7 Syncs Vault secrets into Kubernetes<\/li>\n\n\n<li>Networking CoreDNS \u00b7 DNS resolver for all home network devices Homepage \u00b7 Dashboard linking to all services<\/li>\n<\/ol>\n\n\n<h3 class=\"wp-block-heading\">Domain Setup: rezaops.com<\/h3>\n\n\n<p class=\"wp-block-paragraph\">All services are accessible via subdomains on my own domain:<\/p>\n\n\n<pre class=\"wp-block-code\"><code>http:\/\/grafana.rezaops.com      \u2192 Grafana dashboards\nhttp:\/\/prometheus.rezaops.com   \u2192 Prometheus metrics\nhttp:\/\/jenkins.rezaops.com      \u2192 Jenkins pipelines\nhttp:\/\/argocd.rezaops.com       \u2192 ArgoCD deployments\nhttp:\/\/vault.rezaops.com        \u2192 Vault UI\nhttp:\/\/longhorn.rezaops.com     \u2192 Longhorn storage UI\nhttp:\/\/home.rezaops.com         \u2192 Homepage dashboard<\/code><\/pre>\n\n\n<p class=\"wp-block-paragraph\">These resolve to private IPs (10.0.0.x) : only reachable on my home network. AWS Route 53 stores the records, CoreDNS running inside K3s resolves them for every device on my WiFi.<\/p>\n\n<h3 class=\"wp-block-heading\">OS Setup: Ubuntu 24.04 LTS<\/h3>\n\n\n<p class=\"wp-block-paragraph\">Before installing Kubernetes, both machines need a solid OS foundation. Kubernetes is strict about the environment it runs in \u2014 swap must be off, specific kernel modules must be loaded, and network parameters must be tuned. Get any of these wrong and K3s either refuses to start or behaves unpredictably.<\/p>\n\n\n<p class=\"wp-block-paragraph\">I chose Ubuntu 24.04 LTS for both nodes:<\/p>\n\n\n<ul class=\"wp-block-list\">\n<li>LTS: 5 years of security updates<\/li>\n\n\n<li>Wide support: every tool in the stack has Ubuntu 24.04 support<\/li>\n\n\n<li>Consistency: same OS on both nodes means identical behaviour<\/li>\n<\/ul>\n\n\n<p class=\"wp-block-paragraph\">All OS setup is handled by the common Ansible role, running on both jarvis and thor simultaneously.<\/p>\n\n\n<h3 class=\"wp-block-heading\">Step 1 : Set Hostname<\/h3>\n\n\n<pre class=\"wp-block-code\"><code>- name: Set hostname\n  hostname:\n    name: \"{{ node_name }}\"<\/code><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Sets the machine&#8217;s hostname permanently using hostnamectl. K3s uses the hostname as the node name in the cluster, if wrong, your node shows up with a confusing name in kubectl get nodes.<\/p>\n\n\n<ul class=\"wp-block-list\">\n<li>UM773 \u2192 jarvis<\/li>\n\n\n<li>M1 Plus \u2192 thor<\/li>\n<\/ul>\n\n\n<h3 class=\"wp-block-heading\">Step 2: Update \/etc\/hosts<\/h3>\n\n\n<pre class=\"wp-block-code\"><code>- name: Update \/etc\/hosts with cluster nodes\n  blockinfile:\n    path: \/etc\/hosts\n    marker: \"# {mark} HOMELAB CLUSTER NODES\"\n    block: |\n      10.0.0.148  jarvis\n      10.0.0.149  thor<\/code><\/pre>\n\n\n<p class=\"wp-block-paragraph\">K3s nodes need to resolve each other by hostname for cluster communication. Without this, if DNS is unavailable during boot, the nodes can&#8217;t find each other.<\/p>\n\n\n<p class=\"wp-block-paragraph\">blockinfile adds text between markers, making it idempotent. Running the playbook again updates the block instead of duplicating it.<\/p>\n\n\n<h3 class=\"wp-block-heading\">Step 3: System Update + Essential Packages<\/h3>\n\n\n<pre class=\"wp-block-code\"><code>- name: Update and upgrade apt packages\n  apt:\n    update_cache: yes\n    upgrade: dist\n    cache_valid_time: 3600\n\n- name: Install essential packages\n  apt:\n    name:\n      - curl\n      - wget\n      - git\n      - vim\n      - htop\n      - net-tools\n      - iptables\n      - open-iscsi       # required by Longhorn\n      - nfs-common       # required by Longhorn\n      - ca-certificates\n      - gnupg\n      - lsb-release\n      - unattended-upgrades\n    state: present<\/code><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Two packages here are not optional, open-iscsi and nfs-common are required by Longhorn (distributed storage). Without them, Longhorn volumes never mount and any pod needing persistent storage fails to start.<\/p>\n\n<h3 class=\"wp-block-heading\">Step 4: Disable Swap<\/h3>\n\n\n<pre class=\"wp-block-code\"><code>- name: Disable swap immediately\n  command: swapoff -a\n  when: ansible_swaptotal_mb &gt; 0\n\n- name: Disable swap in fstab permanently\n  replace:\n    path: \/etc\/fstab\n    regexp: '^([^#].*?\\sswap\\s+.*)$'\n    replace: '# \\1'<\/code><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Why Kubernetes requires swap to be disabled: Kubernetes assumes full control over memory allocation on a node. When swap is enabled, the kernel can silently move memory pages to disk, Kubernetes doesn&#8217;t know about this, pod memory limits become unreliable, and the scheduler makes decisions based on incorrect memory availability.<\/p>\n\n\n<p class=\"wp-block-paragraph\">This is one of the most common reasons K3s fails to start on a fresh Ubuntu install \u2014 Ubuntu enables swap by default.<\/p>\n\n\n<p class=\"wp-block-paragraph\">when: ansible_swaptotal_mb &gt; 0, skips the task if swap is already off. Pure idempotency using an automatically gathered Ansible fact.<\/p>\n\n\n<h3 class=\"wp-block-heading\">Step 5: Load Kernel Modules<\/h3>\n\n\n<pre class=\"wp-block-code\"><code>- name: Load required kernel modules\n  modprobe:\n    name: \"{{ item }}\"\n    state: present\n  loop:\n    - overlay\n    - br_netfilter\n\n- name: Persist kernel modules on boot\n  copy:\n    dest: \/etc\/modules-load.d\/k3s.conf\n    content: |\n      overlay\n      br_netfilter<\/code><\/pre>\n\n\n<p class=\"wp-block-paragraph\">overlay, the overlay filesystem driver. Container images are built in layers. This module lets the kernel merge those layers into a unified filesystem that a container sees. Without it, containerd can&#8217;t run containers.<\/p>\n\n\n<p class=\"wp-block-paragraph\">br_netfilter, bridge netfilter. Kubernetes creates virtual network bridges to connect pods. This module allows iptables rules to inspect traffic crossing those bridges. Without it, Kubernetes network policies and pod-to-pod networking don&#8217;t work.<\/p>\n\n\n<p class=\"wp-block-paragraph\">Two steps because modprobe loads modules for the current boot only. Writing to \/etc\/modules-load.d\/k3s.conf makes systemd load them on every subsequent boot.<\/p>\n\n\n<h3 class=\"wp-block-heading\">Step 6: Tune Sysctl Parameters<\/h3>\n\n\n<pre class=\"wp-block-code\"><code>- name: Set sysctl parameters for Kubernetes networking\n  sysctl:\n    name: \"{{ item.key }}\"\n    value: \"{{ item.value }}\"\n    state: present\n    reload: yes\n    sysctl_file: \/etc\/sysctl.d\/99-k3s.conf\n  loop:\n    - { key: \"net.bridge.bridge-nf-call-iptables\",  value: \"1\" }\n    - { key: \"net.bridge.bridge-nf-call-ip6tables\", value: \"1\" }\n    - { key: \"net.ipv4.ip_forward\",                 value: \"1\" }<\/code><\/pre>\n\n\n<p class=\"wp-block-paragraph\">net.bridge.bridge-nf-call-iptables = 1, tells the kernel to pass bridged IPv4 traffic through iptables. When pods communicate across nodes, traffic flows over network bridges. This ensures iptables rules apply to that bridged traffic. Without it, pod networking is broken.<\/p>\n\n\n<p class=\"wp-block-paragraph\">net.ipv4.ip_forward = 1, enables IP forwarding. Allows the Linux kernel to forward packets from one interface to another. Essential for pod-to-pod communication across nodes.<\/p>\n\n\n<p class=\"wp-block-paragraph\">sysctl_file: \/etc\/sysctl.d\/99-k3s.conf writes to a dedicated file instead of \/etc\/sysctl.conf ,keeps our changes isolated and easy to remove later.<\/p>\n\n<h3 class=\"wp-block-heading\">Step 7: Install Docker (Modern Method)<\/h3>\n\n\n<pre class=\"wp-block-code\"><code>- name: Create apt keyrings directory\n  file:\n    path: \/etc\/apt\/keyrings\n    state: directory\n    mode: \"0755\"\n\n- name: Download Docker GPG key\n  shell: |\n    curl -fsSL https:\/\/download.docker.com\/linux\/ubuntu\/gpg \\\n      | gpg --dearmor -o \/etc\/apt\/keyrings\/docker.gpg\n    chmod a+r \/etc\/apt\/keyrings\/docker.gpg\n  args:\n    creates: \/etc\/apt\/keyrings\/docker.gpg\n\n- name: Add Docker apt repository\n  apt_repository:\n    repo: \"deb [arch=amd64 signed-by=\/etc\/apt\/keyrings\/docker.gpg]\n           https:\/\/download.docker.com\/linux\/ubuntu\n           {{ ansible_distribution_release }} stable\"\n    state: present\n    filename: docker\n\n- name: Install Docker packages\n  apt:\n    name:\n      - docker-ce\n      - docker-ce-cli\n      - containerd.io\n      - docker-buildx-plugin\n      - docker-compose-plugin\n    state: present\n    update_cache: yes\n\n- name: Enable and start Docker\n  systemd:\n    name: docker\n    enabled: yes\n    state: started<\/code><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Why not apt-key? Ubuntu 24.04 removed apt-key entirely, it was deprecated in 22.04. The modern approach stores GPG keys in \/etc\/apt\/keyrings\/ and references them in the repo definition with signed-by=. This is also more secure, each repo&#8217;s key is isolated.<\/p>\n\n\n<p class=\"wp-block-paragraph\">args: creates: \/etc\/apt\/keyrings\/docker.gpg &#8211; makes the shell task idempotent. If the file already exists, the download is skipped.<\/p>\n\n\n<p class=\"wp-block-paragraph\">{{ ansible_distribution_release }}, automatically resolves to noble on Ubuntu 24.04. No hardcoding needed.<\/p>\n\n\n<h3 class=\"wp-block-heading\">Step 8: Dev Toolchains on thor Only<\/h3>\n\n\n<pre class=\"wp-block-code\"><code>- name: Install dev toolchains on worker node\n  block:\n    - name: Install Python\n      apt:\n        name: [python3, python3-pip, python3-venv, python3-dev]\n        state: present\n\n    - name: Install Go\n      unarchive:\n        src: https:\/\/go.dev\/dl\/go1.22.4.linux-amd64.tar.gz\n        dest: \/usr\/local\n        remote_src: yes\n        creates: \/usr\/local\/go\/bin\/go\n\n    - name: Add Go to PATH\n      lineinfile:\n        path: \/home\/{{ lab_user }}\/.bashrc\n        line: 'export PATH=$PATH:\/usr\/local\/go\/bin'\n\n    - name: Install Node.js\n      # via NodeSource setup script\n      ...\n\n  when: node_role == \"worker\"<\/code><\/pre>\n\n\n<p class=\"wp-block-paragraph\">when: node_role == &#8220;worker&#8221; at the block level, the entire block is skipped on jarvis. One condition covers every task inside it.<\/p>\n\n\n<p class=\"wp-block-paragraph\">jarvis runs DevOps infrastructure. thor is where I write and run code via VS Code SSH Remote from my MacBook. This gives me a full Python\/Go\/Node.js dev environment on thor without polluting the control plane.<\/p>\n\n\n<h3 class=\"wp-block-heading\">Verify Everything Worked<\/h3>\n\n\n<p class=\"wp-block-paragraph\">After the playbook finishes, SSH into each node:<\/p>\n\n\n<pre class=\"wp-block-code\"><code># Swap is off\nfree -h | grep Swap\n# Swap:  0B  0B  0B \n\n# Kernel modules loaded\nlsmod | grep -E \"overlay|br_netfilter\"\n# overlay    151552  0\n# br_netfilter  32768  0 \n\n# IP forwarding enabled\nsysctl net.ipv4.ip_forward\n# net.ipv4.ip_forward = 1 \n\n# Docker running\ndocker --version &amp;&amp; docker ps \n\n# iSCSI daemon running\nsystemctl is-active iscsid\n# active <\/code><\/pre>\n\n\n<h3 class=\"wp-block-heading\">What&#8217;s Next<\/h3>\n\n\n<p class=\"wp-block-paragraph\">In Part 2 I&#8217;ll walk through the full Ansible project structure . how roles, inventory, and variables are organized, why group_vars caused problems and how we fixed it, and the exact ansible.cfg settings that made everything work.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Why I Built a Home Lab I&#8217;m a Junior DevOps and Cloud Engineer , actively building my skills across Kubernetes, CI\/CD, observability,&hellip;<\/p>\n","protected":false},"author":2,"featured_media":157,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_rezaops_part":"1","_rezaops_linkedin_url":"https:\/\/www.linkedin.com\/pulse\/part-01-why-i-built-home-lab-os-setup-system-reza-chegini-ae8kc\/","footnotes":""},"categories":[10,62,74],"tags":[8,96,66,76,38],"series":[64],"class_list":["post-155","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops","category-home-lab","category-linux","tag-devops","tag-hardening","tag-home-lab","tag-linux","tag-security","series-home-lab"],"jetpack_featured_media_url":"https:\/\/rezachegini.com\/wp-content\/uploads\/2026\/10\/part-01-why-i-built-home-lab-cover.png","_links":{"self":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts\/155","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=155"}],"version-history":[{"count":1,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts\/155\/revisions"}],"predecessor-version":[{"id":156,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/posts\/155\/revisions\/156"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=\/wp\/v2\/media\/157"}],"wp:attachment":[{"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=155"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=155"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=155"},{"taxonomy":"series","embeddable":true,"href":"https:\/\/rezachegini.com\/index.php?rest_route=%2Fwp%2Fv2%2Fseries&post=155"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}